Git Product home page Git Product logo

honeybear.halclient's Introduction

HoneyBear.HalClient

Build Status Coverage Status NuGet Version

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>();
  1. GET https://api.retail.com/v1/version/1
  2. GET https://api.retail.com/v1/order/46AC5C29-B8EB-43E7-932E-19167DA9F5D3
  3. 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;
  1. GET https://api.retail.com/v1/version/1
  2. GET https://api.retail.com/v1/order/46AC5C29-B8EB-43E7-932E-19167DA9F5D3
  3. Reads Order resource
  4. 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>();
  1. GET https://api.retail.com/v1/version/1
  2. GET https://api.retail.com/v1/order?pagenumber=0
  3. 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();
  1. GET https://api.retail.com/v1/version/1
  2. GET https://api.retail.com/v1/order?pagenumber=0
  3. Reads embedded array of Order resources
  4. Deserialises resources into a list of Orders

5) Create a resource

var payload = new { ... };

Order order =
    client
        .Root("/v1/version/1")
        .Post("order-add", payload, "retail")
        .Item<Order>()
        .Data;
  1. GET https://api.retail.com/v1/version/1
  2. POST https://api.retail.com/v1/order (with payload)
  3. Reads Order resource from response
  4. 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;
  1. GET https://api.retail.com/v1/version/1
  2. GET https://api.retail.com/v1/order/46AC5C29-B8EB-43E7-932E-19167DA9F5D3
  3. PUT https://api.retail.com/v1/order/46AC5C29-B8EB-43E7-932E-19167DA9F5D3 (with payload)
  4. Reads Order resource from response
  5. 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");
  1. GET https://api.retail.com/v1/version/1
  2. GET https://api.retail.com/v1/order/46AC5C29-B8EB-43E7-932E-19167DA9F5D3
  3. 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;
  1. GET https://api.retail.com/v1/version/1
  2. GET https://api.retail.com/v1/order/46AC5C29-B8EB-43E7-932E-19167DA9F5D3
  3. Reads Order resource
  4. 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
          }
        }
      }
    ]
  }
}

honeybear.halclient's People

Contributors

berhir avatar bobhonores avatar christianz avatar dependabot[bot] avatar dmunch avatar eoin55 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

honeybear.halclient's Issues

Be able to specify which MediaTypeFormatter to use when PUT or POSTing

Hi!

I downloaded the latest version of the nuget package, and everything seems to work fine when fetching data from the API. But I'm still not able to specify the serializer settings when POSTing to the API.

In JsonHttpClient, these lines should also take in a MediaTypeFormatter:

public Task<HttpResponseMessage> PostAsync<T>(string uri, T value)
  => HttpClient.PostAsync(uri, value, _mediaTypeFormatter);

public Task<HttpResponseMessage> PutAsync<T>(string uri, T value)
  => HttpClient.PutAsync(uri, value, _mediaTypeFormatter);

I'm not sure what the best strategy is to use here. I see that you allow a collection of MediaTypeFormatters to be passed into the HalClient constructor. But we can only specify a single formatter when serializing objects. Should it instantiate the JsonHttpClient with the First() available MediaTypeFormatter?

Christian

Should accept application/hal+json as response content type

Hi, and thanks for a great HAL library.

Some APIs only return content with Content-Type: application/hal+json. Trying to consume these with HalClient fails, as this software only seems to accept application/json.

A fix for this would be to let the HalClient instantiation accept a System.Net.Http.Formatters.MediaTypeFormatter as a constructor parameter and use this for all serialization/deserialization for the current session.

I have some snippets that would allow for this, but would like to hear your thoughts on this before I proceed.

NUnit 2.6.4 in VS2015

Hi, I have started a pull request to allow JsonPropertyAttributes on model classes ( #6 ) but I have problems to run the NUnit tests in Visual Studio 2015.
I can run it with NUnit outside of Visual Studio but some tests fail because the assembly binding redirect of Ploeh.SemanticComparison doesn't work.

I would like to add some tests for this new functionality.
Do you have an idea why it doesn't work?

Support for .NET 4.5

Hi,

I really like this fluent API. It'll be nice to have support for .NET 4.5 and above, instead only limited to 4.5.2.

Thanks.

Regards,

Roberto

Retrieve single objects

Hi, i found your library and the api looks very nice for consuming hal based rest apis.

I have two questions regarding the api usage:

  1. Retrieve a single resource
    My Resource:
{
  "_embedded" : {
    "users" : [ {
      "id" : 2,
      "name" : "asddf",
      "_links" : {
        "self" : {
          "href" : "http://localhost:8080/users/2"
        },
        "user" : {
          "href" : "http://localhost:8080/users/2"
        },
        "address" : {
          "href" : "http://localhost:8080/users/2/address"
        }
      }
    } ]
  },
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/users{?page,size,sort}",
      "templated" : true
    },
    "profile" : {
      "href" : "http://localhost:8080/profile/users"
    }
  },
  "page" : {
    "size" : 20,
    "totalElements" : 1,
    "totalPages" : 1,
    "number" : 0
  }
}

How do i get the user resource with id 2 without fetching everything and traverse through the result? I am looking for a single http request solution to get /users/2 into my user dto.

  1. How is error handling solved?
    How do i check for all the http error codes and messages e.g.:
    /users/3 => 404 or with detailed json result.

Problem getting empty list from _embedded

Hi,
thanks for a really useful project.
I am still rather new at this so please consider that, it might be lack of understanding from my side. I have encountered one issue where I am not able to get an embeded object (see example below) if that object is empty (empty list). My understanding is that empty list is OK to be considered a "resource". Moreover, I cant handle this by catching the HoneyBear.HalClient.Models.FailedToResolveRelationship exception explicitly since that exception is "internal" - what is the reason to this design choice ? Any advice how this situation should be handled? Thanks in advance!
br
/Kazze

{ "_embedded": { "availableServices": [] }, "_links": { "self": { "href": "xx" } } }

Fill DTO Object with Elements from the resource and embedded Objects?

hello,
is it possible to retrive response data (in the example the "mainDTO") from the resource and embedded objects in one single call?

example:

using Newtonsoft.Json;
public class SubDTO{
    [JsonProperty("id")]
    public int ID { get; set; }
    [JsonProperty("name")]
    public string Name { get; set; }
}

using Newtonsoft.Json;
public class mainDTO{
    [JsonProperty("id")]
    public int ID { get; set; }
    [JsonProperty("name")]
    public string Name { get; set; }
    [JsonProperty("subDTOs")]
    public SubDTO[] SubDTOs { get; set; }
} 

json:

{
	"id": 12,
	"name": "my name",
	"_embedded": {
		"subDTOs": [{
			"id": 1,
			"name": "xyz"
		}, {
			"id": 1,
			"name": "xyz"
		}]
	}
}

How do I access nested Links?

Please help!

I cannot work out what I am supposed to do to get at nested Resources.

This code works
var allLocationLinks = halClient.Root().Get("visibleLocations").Item<AllLocations>().Data;
and returns 1000+ Locations with ID and Name but but I can't work out how to get at the Links to add the Advisers.

Cheers
Simon

Root JSON

{
  "_links": {
    "transactionMappingHierarchy": {
      "href": "https://XXX/nbs/transactionMappingHierarchy"
    },
    "accountByAccountNumber": {
      "href": "https://XXX/nbs/accounts/{accountNumber}",
      "templated": true
    },
    "accountByAccountId": {
      "href": "https://XXX/nbs/accounts/ids/{accountId}",
      "templated": true
    },
    "client": {
      "href": "https://XXX/nbs/clients/{clntId}",
      "templated": true
    },
    "visibleLocations": {
      "href": "https://XXX/nbs/locations"
    },
    "docs": {
      "href": "https://XXX/docs"
    }
  }
}

JSON from visibleLocations
{
  "locations": [
    {
      "locationId": 40000003,
      "name": "1000 IFA Firm 40000003 Ltd",
      "_links": {
        "self": {
          "href": "https://XXX/nbs/locations/40000003"
        },
        "advisers": {
          "href": "https://XXX/nbs/locations/40000003/advisers"
        }
      }
    },
    {
      "locationId": 100002441,
      "name": "10000 IFA Firm 100002441 Ltd",
      "_links": {
        "self": {
          "href": "https://XXX/nbs/locations/100002441"
        },
        "advisers": {
          "href": "https://XXX/nbs/locations/100002441/advisers"
        }
      }
    }, ...etc...

My POCO classes

	public class AllLocations
	{
		public Location[] Locations { get; set; }
	}

	public class Location
	{
		[JsonProperty("locationId")]
		public int ID { get; set; }

		public string Name { get; set; }

		[JsonProperty("advisers")]
		public Adviser[] Advisers { get; set; }
	}

	public class Adviser
	{
		[JsonProperty("adviserID")]
		public int ID { get; set; }

		public string Forename { get; set; }

		public string Surname { get; set; }

		[JsonProperty("adviserOutletID")]
		public int OutletID { get; set; }
	}

HAL Forms

I see this lib will POST/PUT etc based on the href or a rel. Any plans to make it work with HAL-FORMS, a media type that returns meta data about required fields for POST/PUT verbs?

https://rwcbook.github.io/hal-forms/

Async support

Hi,

and thanks again for this great library, the fluent interface really is fun to use!

While using the first time I kind of expected the library to fully support async/await and was surprised it didn't. I figured this might be a nice excersice, so I went ahead trying to implement it.

Turns out going async and keeping the fluent interface wasn't straight-forward. The resulting pull-request (which I didn't send in yet, since it depends on #17) refactors quite a bit in the code, as such I understand you might hesitate merging or even question the approach taken.

Outlining it roughly:

  • HalClient is immutable, i.e. all the calls return a new copy with the new state
  • Most of the methods are extension methods operating on IHalClient, allowing easy chaining of Task<IHalClient>'s.

While the changes in the implementation are rather important, fortunately the tests behaved pretty nicely and only needed some slight changes, mainly for the immutable part. Also, adding tests for the async methods was as easy as copying the existing ones and only changing the sync method calls to the async method calls. Kudos!

You can find call the changes on this branch:
https://github.com/dmunch/HoneyBear.HalClient/tree/dmunch-async

If you decide to accept merging those changes there's a tiny list of things yet to be done. Of course I'd happily take care of these.

  • Add POST
  • Add PUT
  • Add PATCH
  • Add DELETE
  • Adjust documentation

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.