A lightweight fluent .NET client for navigating and consuming HAL APIs.
HAL (Hypertext Application Language) is a specification for a lightweight hypermedia type.
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.
- HoneyBear.HalClient only supports the JSON HAL format.
If you have any issues, suggests or comments, please create an issue or a pull request.
Install-Package HoneyBear.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/");
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() });
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);
The following examples are based on the example JSON below.
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
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
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
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
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
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
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
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.
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>();
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
}
}
}
]
}
}