HoneyBear.HalClient
HoneyBear.HalClient copied to clipboard
A lightweight fluent .NET client for navigating and consuming HAL APIs.
HoneyBear.HalClient
A lightweight fluent .NET client for navigating and consuming HAL APIs.
What is HAL?
HAL (Hypertext Application Language) is a specification for a lightweight hypermedia type.
What's Nice About this HAL Client
There are already a number of open-source .NET HAL clients available. HoneyBear.HalClient differs because it offers all of the following features:
- Provides a fluent-like API for navigating a HAL API.
- No additional attributes or semantics are required on the API contract. Resources can be deserialised into POCOs.
- Supports the Hypertext Cache Pattern; it treats embedded resources in the same way as it handles links.
- Supports URI templated links. It uses Tavis.UriTemplates under the hood.
Known Limitations
- HoneyBear.HalClient only supports the JSON HAL format.
Feedback Welcome
If you have any issues, suggests or comments, please create an issue or a pull request.
Getting Started
1) Install the NuGet package
Install-Package HoneyBear.HalClient
2) Create an instance of HalClient
HalClient
has a dependency on HttpClient
. This can be provided in the constructor:
var halClent = new HalClient(new HttpClient { BaseAddress = new Uri("https://api.retail.com/") });
Or accessed via a public property:
var halClent = new HalClient();
halClent.HttpClient.BaseAddress = new Uri("https://api.retail.com/");
(Optional) Custom serializer settings
HalClient uses the default JsonMediaTypeFormatter for handling deserialization of responses. If you need to change any of the settings (for handling null values, missing properties, custom date formats and so on), you can build a custom MediaTypeFormatter by subclassing JsonMediaTypeFormatter, and then passing it in to the HalClient constructor:
public class CustomMediaTypeFormatter : JsonMediaTypeFormatter
{
SerializerSettings.NullValueHandling = NullValueHandling.Ignore;
SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/hal+json"));
}
var halClent = new HalClient(new HttpClient { BaseAddress = new Uri("https://api.retail.com/") }, new List<MediaTypeFormatter> { new CustomMediaTypeFormatter() });
(Optional) Override default implementation of IJsonHttpClient
By default, HalClient
uses a internal implementation of IJsonHttpClient
, which uses HttpClient
to perform HTTP requests (GET, POST, PUT and DELETE). In some cases, it may be preferable to provide your own implementation of IJsonHttpClient
. For example, if you want to specify a different MediaTypeFormatter
for serializing POST and PUT requests:
public class CustomJsonHttpClient : IJsonHttpClient
{
private readonly CustomMediaTypeFormatter _formatter;
public CustomJsonHttpClient(HttpClient client, CustomMediaTypeFormatter formatter)
{
HttpClient = client;
_formatter = formatter;
HttpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/hal+json"));
}
public HttpClient HttpClient { get; }
public Task<HttpResponseMessage> GetAsync(string uri)
=> HttpClient.GetAsync(uri);
public Task<HttpResponseMessage> PostAsync<T>(string uri, T value)
=> HttpClient.PostAsync(uri, value, _formatter);
public Task<HttpResponseMessage> PutAsync<T>(string uri, T value)
=> HttpClient.PutAsync(uri, value, _formatter);
public Task<HttpResponseMessage> DeleteAsync(string uri)
=> HttpClient.DeleteAsync(uri);
}
var jsonClient = new CustomJsonHttpClient(new HttpClient(), new CustomMediaTypeFormatter());
var halClent = new HalClient(jsonClient);
or
var jsonClient = new CustomJsonHttpClient(new HttpClient(), new CustomMediaTypeFormatter());
var formatters = new List<MediaTypeFormatter> { new CustomMediaTypeFormatter() };
var halClent = new HalClient(jsonClient, formatters);
Usage Examples
The following examples are based on the example JSON below.
1) Retrieve a single resource
IResource<Order> order =
client
.Root("/v1/version/1")
.Get("order", new {orderRef = "46AC5C29-B8EB-43E7-932E-19167DA9F5D3"}, "retail")
.Item<Order>();
- GET https://api.retail.com/v1/version/1
- GET https://api.retail.com/v1/order/46AC5C29-B8EB-43E7-932E-19167DA9F5D3
- Reads Order resource
2) Deserialise that resource into a POCO
Order order =
client
.Root("/v1/version/1")
.Get("order", new {orderRef = "46AC5C29-B8EB-43E7-932E-19167DA9F5D3"}, "retail")
.Item<Order>()
.Data;
- GET https://api.retail.com/v1/version/1
- GET https://api.retail.com/v1/order/46AC5C29-B8EB-43E7-932E-19167DA9F5D3
- Reads Order resource
- Deserialises resource into
Order
3) Retrieve a list of resources (embedded in a paged list resource)
IEnumerable<IResource<Order>> orders =
client
.Root("/v1/version/1")
.Get("order-query", new {pageNumber = 0}, "retail")
.Get("order", "retail")
.Items<Order>();
- GET https://api.retail.com/v1/version/1
- GET https://api.retail.com/v1/order?pagenumber=0
- Reads embedded array of Order resources
4) Deserialise the list of resources into POCOs
IEnumerable<Order> orders =
client
.Root("/v1/version/1")
.Get("order-query", new {pageNumber = 0}, "retail")
.Get("order", "retail")
.Items<Order>()
.Data();
- GET https://api.retail.com/v1/version/1
- GET https://api.retail.com/v1/order?pagenumber=0
- Reads embedded array of Order resources
- Deserialises resources into a list of
Order
s
5) Create a resource
var payload = new { ... };
Order order =
client
.Root("/v1/version/1")
.Post("order-add", payload, "retail")
.Item<Order>()
.Data;
- GET https://api.retail.com/v1/version/1
- POST https://api.retail.com/v1/order (with payload)
- Reads Order resource from response
- Deserialises resource into
Order
6) Update a resource
var payload = new { ... };
Order order =
client
.Root("/v1/version/1")
.Get("order", new {orderRef = "46AC5C29-B8EB-43E7-932E-19167DA9F5D3"}, "retail")
.Put("order-edit", payload, "retail")
.Item<Order>()
.Data;
- GET https://api.retail.com/v1/version/1
- GET https://api.retail.com/v1/order/46AC5C29-B8EB-43E7-932E-19167DA9F5D3
- PUT https://api.retail.com/v1/order/46AC5C29-B8EB-43E7-932E-19167DA9F5D3 (with payload)
- Reads Order resource from response
- Deserialises resource into
Order
7) Delete a resource
client
.Root("/v1/version/1")
.Get("order", new {orderRef = "46AC5C29-B8EB-43E7-932E-19167DA9F5D3"}, "retail")
.Delete("order-delete", "retail");
- GET https://api.retail.com/v1/version/1
- GET https://api.retail.com/v1/order/46AC5C29-B8EB-43E7-932E-19167DA9F5D3
- DELETE https://api.retail.com/v1/order/46AC5C29-B8EB-43E7-932E-19167DA9F5D3
8) Retrieve a resource's links
IList<ILink> links =
client
.Root("/v1/version/1")
.Get("order", new {orderRef = "46AC5C29-B8EB-43E7-932E-19167DA9F5D3"}, "retail")
.Item<Order>()
.Links;
- GET https://api.retail.com/v1/version/1
- GET https://api.retail.com/v1/order/46AC5C29-B8EB-43E7-932E-19167DA9F5D3
- Reads Order resource
- Returns the Order resource's links, e.g. self, retail:order-edit, retail:order-delete.
Dependency Injection
HalClient
implements interface IHalClient
. Registering it with Autofac might look something like this:
builder
.RegisterType<HttpClient>()
.WithProperty("BaseAddress", new Uri("https://api.retail.com"))
.AsSelf();
builder
.RegisterType<HalClient>()
.As<IHalClient>();
Example JSON
Root resource: https://api.retail.com/v1/version/1
{
"versionNumber": 1,
"_links": {
"curies": [
{
"href": "https://api.retail.com/v1/docs/{rel}",
"name": "retail",
"templated": true
}
],
"self": {
"href": "/v1/version/1"
},
"retail:order-query": {
"href": "/v1/order?pageNumber={pageNumber}&pageSize={pageSize}",
"templated": true
},
"retail:order": {
"href": "/v1/order/{orderRef}",
"templated": true
},
"retail:order-add": {
"href": "/v1/order"
},
"retail:order-queryby-user": {
"href": "/v1/order?userRef={userRef}",
"templated": true
}
}
}
Order resource: https://api.retail.com/v1/order/46AC5C29-B8EB-43E7-932E-19167DA9F5D3
{
"orderRef": "46ac5c29-b8eb-43e7-932e-19167da9f5d3",
"orderNumber": "123456",
"status": "AwaitingPayment",
"total": {
"amount": 100.0,
"currency": "USD"
},
"_links": {
"curies": [
{
"href": "https://api.retail.com/v1/docs/{rel}",
"name": "retail",
"templated": true
}
],
"self": {
"href": "/v1/order/46ac5c29-b8eb-43e7-932e-19167da9f5d3"
},
"retail:order-edit": {
"href": "/v1/order/46ac5c29-b8eb-43e7-932e-19167da9f5d3"
},
"retail:order-delete": {
"href": "/v1/order/46ac5c29-b8eb-43e7-932e-19167da9f5d3"
},
"retail:orderitem": {
"href": "/v1/orderitem"
}
},
"_embedded": {
"retail:orderitem": [
{
"orderItemRef": "d7161f76-ed17-4156-a627-bc13b43345ab",
"status": "AwaitingPayment",
"total": {
"amount": 20.0,
"currency": "USD"
},
"quantity": 1,
"_links": {
"self": {
"href": "/v1/orderitem"
},
"retail:product": {
"href": "/v1/product/637ade4e-e927-4d4a-a628-32055ae5a12b"
}
}
},
{
"orderItemRef": "25d61931-181b-4b09-b883-c6fb374d5f4a",
"status": "AwaitingPayment",
"total": {
"amount": 30.0,
"currency": "USD"
},
"quantity": 2,
"_links": {
"self": {
"href": "/v1/orderitem"
},
"retail:product": {
"href": "/v1/product/fdc0d414-23a1-4208-a20a-9eeab0351f76"
}
}
}
]
}
}
Paged list of Orders resource: https://api.retail.com/v1/order?pageNumber=0
{
"pageNumber": 0,
"pageSize": 10,
"knownPagesAvailable": 1,
"totalItemsCount": 1,
"_links": {
"curies": [
{
"href": "https://api.retail.com/v1/docs/{rel}",
"name": "retail",
"templated": true
}
],
"self": {
"href": "/v1/order?pageNumber=0&pageSize=10"
},
"retail:order": {
"href": "/v1/order/{orderRef}",
"templated": true
}
},
"_embedded": {
"retail:order": [
{
"orderRef": "e897113c-4c56-404b-8e83-7e7f705046b3",
"orderNumber": "789456",
"status": "AwaitingPayment",
"total": {
"amount": 100.0,
"currency": "USD"
},
"_links": {
"self": {
"href": "/v1/order/e897113c-4c56-404b-8e83-7e7f705046b3"
},
"retail:order-edit": {
"href": "/v1/order/e897113c-4c56-404b-8e83-7e7f705046b3"
},
"retail:order-delete": {
"href": "/v1/order/e897113c-4c56-404b-8e83-7e7f705046b3"
},
"retail:orderitem-queryby-order": {
"href": "/v1/orderitem?pageNumber={pageNumber}&pageSize={pageSize}&orderRef=e897113c-4c56-404b-8e83-7e7f705046b3",
"templated": true
}
}
}
]
}
}