Click here to Skip to main content
Click here to Skip to main content

A Dynamic Rest Client Proxy with the DLR

, 8 Oct 2014 CPOL
Rate this:
Please Sign up or sign in to vote.
An easy to use Rest client using the Dynamic Language Runtime

Latest source code on GitHub

NuGet Package

Introduction

When I come across an interesting Rest web service that I want to explore or maybe integrate into an app, the first thing that needs to be done is to create a bunch of wrapper classes are out the http communication so that the meat of the service can be invoked. This usually looks something like this:

  • Read the API docs
  • Look at pre-supplied .NET library (if any) and decide it doesn't fit the rest of the programming model, so write a wrapper
  • Create some service classes to mirror the endpoints of the API
  • Create a bunch of POCO objects to represent the data going back and forth
  • Fiddle around with that for a bit until data is flowing
  • Actually do something interesting with the API

Even with great tools like RestSharp and Json2CSharp, I always find myself writing a lot of boilerplate code before getting down to the fun.

This little project grew out of my boredom with the boilerplate, plus a desire to explore the Dynamic Language Runtime (DLR). The result is a convention based dynamic proxy around the RestShap RestClient (which handles all the http) that makes it easy to interact with Rest Services with minimal startup overhead.

Background

The basic premise is that the RestProxy is a DynamicObject that translates property and method invocations into RestRequests. A DyanmicObject generates its members at runtime and it's this capability that is used to build up the request and execute it.

One downside of dynamic objects is the lack of IntelliSense since the IDE does not know what members the object has or will have. It feels more like JavaScript than C#.

Using the code

Client Conventions

  • All communication is via http or https
  • Data transfer is always JSON
  • The vast majority of API access can be accomplished with GET, PUT, POST or DELETE
  • Unnamed arguments passed to a verb invocation are serialized to the request body
  • Named argumeents are passed as request parameters (eitther query params or form encoded)
  • Outputs are dynamic objects
  • All Rest calls are asynchronous and awaitable (they always return a Task<dynamic>)

Calling Conventions

Calls to the proxy all take the following pattern:

proxy.{optional chain of dot separated property names}.verb({optional parameter list});
  • Each property name represents a Url segment relative to the root url
  • verb must be one of get, put, post, delete
  • unnamed arguments to the verb invocation will be serialized into the request body
  • named arguments to the verb invocation will be added as named parameters

So getting up and running with a new service endpoint is 3 steps:

  1. Create a RestClient to represent the API root and handle things like authentication
  2. Wrap it in a DynamicProxy
  3. Invoke away!

Example

So let's try a simple GET example using SunLight Labs API:

var client = new RestClient("http://openstates.org/api/v1"); 
client.AddDefaultHeader("X-APIKEY", "your_api_key_goes_here");
 
dynamic proxy = new RestProxy(client); 
dynamic result = await proxy.metadata.mn.get();
 
Assert.IsNotNull(result);
Assert.IsTrue(result.name == "Minnesota"); 

So what's going on here? The first three lines are pretty self explanatory; create the RestClient and wrap it in a RestProxy.

The fourth line is where all the dyanmic stuff is happening.

The endpoint we are ultimately invoking is this: http://openstates.org/api/v1/metadata/mn/.

See the pattern? metadata.mn gets translated to metadata/mn/ and the get() defines the type of http request invoked. Were you to look at the http request that line creates you'd see:

GET http://openstates.org/api/v1/metadata/mn/ HTTP/1.
Accept: application/json, text/json, text/x-json, text/javascript
X-APIKEY: your_api_key_goes_here
User-Agent: RestSharp/104.4.0.0
Accept-Encoding: gzip, deflate
Host: openstates.org

When a DynamicObject has a property accessed a method called TryGetMember is invoked. It's in here that we create a chain of other DynamicObjects.

public override bool TryGetMember(GetMemberBinder binder, out object result)
{
     // this gets invoked when a dynamic property is accessed
     // example: proxy.locations will invoke here with a binder named location
     // each dynamic property is treated as a url segment     
     result = new RestProxy(_client, this, binder.Name, KeywordEscapeCharacter);     
     return true; 
}

But Wait! Urls can have spaces and all sorts of other crazy characters in them!!!

That's where we introduce an escape method. In order to add a url segment that is not a valid C# identifier, escape it by passing it as an argument to or any method on the dynamic proxy object. Escaped segments can be chained and intermixed with property segments in any combination.

var client = new RestClient("http://openstates.org/api/v1");
client.AddDefaultHeader("X-APIKEY", "your_api_key_goes_here");
dynamic proxy = new RestProxy(client);

var result = await proxy.bills.mn("2013s1")("SF 1").get();
Assert.IsNotNull(result);
Assert.IsTrue(result.id == "MNB00017167");

This has the added advantage of allowing us to add segments that are data not code. For instance perhaps part of the endpoint is determined by user selection (picking one state over another in the examples above) or it is a value returned from a previous call.

string billId = GetBillIdFromUser();
var result = await proxy.mn("2013s1")(billId).get();

Segment Chaining

Notice the somewhat odd proxy.bills.mn("2013s1")("SF 1"). What is going on there?

Until one of the four http verbs is invoked, every method call or property access on the DynamicProxy returns another instance of a DynamicProxy that are chained together forming the entire url.

  • proxy.bills returns a new proxy object from the property accessor "bills"
  • bills.mn("2013s1") invokes the dynamic method "mn" on the bills instance, passing "2013s1" as an argument
  • both mn and "2013s1" are returned as proxy object instances
  • the final part mn("2013s1")("SF 1") is invoking the 2013s1 instance as if it were a delegate. This too returns another instance of a proxy object added to the chain

Each proxy instance represents a segment in the endpoint url. Segment names are defined as the dynamic property names, method names and/or arguments passed to dynamic method invocations.

Passing Parameters

Named parameters are passed to the verb method using C#'s named argument syntax. Here's an example using Bing's Locations API:

var client = new RestClient("http://dev.virtualearth.net/REST/v1/");
client.AddDefaultParameter("key", "bing_key");

dynamic proxy = new RestProxy(client);
var result = await proxy.Locations.get(postalCode: "55116", countryRegion: "US");

Assert.AreEqual((int)result.statusCode, 200);

The http request for the above looks like this:

GET http://dev.virtualearth.net/REST/v1/Locations?postalCode=55116&countryRegion=US&key=bing_key&o=json HTTP/1.1
Accept: application/json, text/json, text/x-json, text/javascript
User-Agent: RestSharp/104.4.0.0
Host: dev.virtualearth.net
Accept-Encoding: gzip, deflate
Connection: Keep-Alive

Escaping Parameter Names

Parameter names are not always going to be valid C# identifiers either (though in practice they are most of the time). Since we are using C#'s named argument syntax to name our request parameters, this represents a problem. Take for instance this endpoint:

congress.api.sunlightfoundation.com/bills?chamber=senate&history.house_passage_result=pass

It has a parameter name with a "." in it. In order to escape parameter names they can be passed to the invoke functions in a dictionary. Any named parameter that is an IDictionary<string, object> will have each key/value pair added as a parameter. This example code will generate the rest request above:

var client = new RestClient("http://congress.api.sunlightfoundation.com");
client.AddDefaultHeader("X-APIKEY", CredentialStore.Key("sunlight"));

dynamic proxy = new RestProxy(client);

var parameters = new Dictionary<string, object>()
{
    { "chamber", "senate" },
    { "history.house_passage_result", "pass" }
};

dynamic result = await proxy.bills.get(paramList: parameters);

foreach (dynamic bill in result.results)
{
    Assert.AreEqual("senate", (string)bill.chamber);
    Assert.AreEqual("pass", (string)bill.history.house_passage_result);
}

Passing Objects

Putting and posting often requires an object in the request body. In order to accomplish that pass an unamed argument to the verb (named and unnamed arguments can be used together but all unnamed arguments must proceed the named ones).

In this example a new Google calendar is created using a POST method. We're using an ExpandObject because we don't have static POCO types. That way both input and output objects can be completely dynamic.

var client = new RestClient("https://www.googleapis.com/calendar/v3");
client.Authenticator = new OAuth2AuthorizationRequestHeaderAuthenticator(_token);
dynamic proxy = new RestProxy(client);

dynamic calendar = new ExpandoObject();
calendar.summary = "unit_testing";

var list = await proxy.calendars.post(calendar);

Assert.IsNotNull(list);
Assert.AreEqual((string)list.summary, "unit_testing");

The resulting http request, notice the serialized object in the body:

POST https://www.googleapis.com/calendar/v3/calendars HTTP/1.1
Authorization: OAuth ya29.1.AADtN_U5OItd_GtTLneoJNAqYXgu8Ad6dzVPF--bcngxtorseu4y1mQYrCSbdCQ
Accept: application/json, text/json, text/x-json, text/javascript
User-Agent: RestSharp/104.4.0.0
Content-Type: application/json
Host: www.googleapis.com
Content-Length: 26
Accept-Encoding: gzip, deflate

{"summary":"unit_testing"}

Verb Invocation

The invoking of the verbs results in a dynamic method call, which ultimately calls TryInvokeMember. The arguments passed to TryInvokeMember contain the details about the dynamic method, its name and arguments. It is in this method where the RestRequest is created, formatted and its invocation wrapped in a Task<dynamic>.

public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
{
    Debug.Assert(binder != null);
    Debug.Assert(args != null);

    //
    if (binder.IsVerb())
    {
        // build a rest request based on this instance, parent instances and invocation arguments
        var builder = new RequestBuilder(this);
        var request = builder.BuildRequest(binder, args);

        // the binder name (i.e. the dynamic method name) is the verb
        // example: proxy.locations.get() binder.Name == "get"
        var invocation = new RestInvocation(_client, binder.Name);
        result = invocation.InvokeAsync(request); // this will return a Task<dynamic> with the rest async call
    }
    else
    {
        if (args.Length != 1)
            throw new InvalidOperationException("The segment escape sequence must have exactly 1 unnamed parameter");

        // this is for when we escape a url segment by passing it as an argument to a method invocation
        // example: proxy.segment1("escaped")
        // here we create two new dynamic objects, 1 for "segment1" which is the method name
        // and then we create one for the escaped segment passed as an argument - "escaped" in the example
        var tmp = new RestProxy(_client, this, binder.Name, KeywordEscapeCharacter);
        result = new RestProxy(_client, tmp, args[0].ToString(), KeywordEscapeCharacter);
    }

    return true;
}

Update

Since writing the orignal version an additional proxy implementation has been added that wraps the portable HttpClient. It works exactly like the RestSharp proxy but uses HttpClient as the communication mechanism. Being portable it is usable on WinRT, Silverlight and Windows Phone.

This implementation also include a completely encapsulated dynamic rest client class. This ccompletely wraps the communication and doesn't require that the caller create and configure the HttpClient instance. This reduces the simplest rest requests to just a couple of lines of code:

dynamic client = new DynamicRestClient("http://dev.virtualearth.net/REST/v1/");

dynamic result = await client.Locations.get(postalCode: "55116", countryRegion: "US", key: CredentialStore.Key("bing"));

Both of these have a NuGet packages.

Updated code is on GitHub and additional examples are avaiable on the Wiki there.

Points of Interest

DynamicObject really has some interesting possibilities and once you wrap your head around is very simple to use. All of the above is done in about 70 lines code.

The Unit Tests

The unit tests have further examples of the various verbs, dynamic OAuth2 and miscellaneous other stuff. If you try to run the unit tests take a close look at the CredentialStore class in the unit test project. It's pretty straightforward and you can use it to supply your own api keys while keeping them out of the code. Virtually all of the integration tests require an api key for the endpoints they hit, so most will fail spectacularly without them.

History

  • 4/20/3014 - initial version
  • 4/24/2014 - parameter name escape mechanism
  • 6/21/2014 - HttpClient addition

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Don Kackman
Team Leader Starkey Laboratories
United States United States
The first computer program I ever wrote was in BASIC on a TRS-80 Model I and it looked something like:
10 PRINT "Don is cool"
20 GOTO 10
It only went downhill from there.
 
Hey look, I've got a blog

Comments and Discussions

 
QuestionNeeds more Details Pinmemberoknaru9-Oct-14 19:07 
AnswerRe: Needs more Details PinmemberDon Kackman11-Oct-14 2:33 
GeneralMy vote of 5 PinmemberM Rayhan16-Jul-14 20:47 
GeneralRe: My vote of 5 PinmemberDon Kackman17-Jul-14 10:22 
QuestionNice Don ! PinprofessionalVolynsky Alex22-Jun-14 8:12 
AnswerRe: Nice Don ! PinmemberDon Kackman22-Jun-14 15:28 
GeneralRe: Nice Don ! PinprofessionalVolynsky Alex22-Jun-14 15:34 
GeneralMy vote of 5 PinprofessionalAssil21-Jun-14 6:53 
GeneralRe: My vote of 5 PinmemberDon Kackman21-Jun-14 11:46 
GeneralMy vote of 5 PinprofessionalPrasad Khandekar27-Apr-14 10:19 
GeneralRe: My vote of 5 PinmemberDon Kackman28-Apr-14 4:25 
QuestionLooks nice PinmemberSteve Solomon23-Apr-14 23:33 
AnswerRe: Looks nice PinmemberDon Kackman24-Apr-14 7:55 
AnswerRe: Looks nice PinmemberDon Kackman27-Apr-14 8:14 
QuestionA truly useful tool PinmvpSacha Barber20-Apr-14 22:35 
AnswerRe: A truly useful tool PinmemberDon Kackman21-Apr-14 4:00 
QuestionExcellent and useful - many thanks - 5* PinprofessionalDuncan Edwards Jones20-Apr-14 13:11 
AnswerRe: Excellent and useful - many thanks - 5* PinmemberDon Kackman21-Apr-14 4:00 
GeneralMy vote of 5 PinprofessionalVolynsky Alex20-Apr-14 4:13 
GeneralRe: My vote of 5 PinmemberDon Kackman20-Apr-14 4:52 
GeneralRe: My vote of 5 PinprofessionalVolynsky Alex20-Apr-14 7:04 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web04 | 2.8.141220.1 | Last Updated 8 Oct 2014
Article Copyright 2014 by Don Kackman
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid