-
Notifications
You must be signed in to change notification settings - Fork 15
Constructing endpoint paths
These patterns apply to any class that derives from DynamicRestProxy but we'll use the DynamicRestClient for this example.
A typical rest endpoint path might contain a mixture of static structure, api versioning and data (identifiers specifying which remote objects to operate on). Take for example the google cloud storage object insert endpoint:
https://www.googleapis.com/upload/storage/v1/b/{bucket}/o
which is a mix of static structure (i.e. a namespace hierarchy to drill through that will never change for a given version) https://www.googleapis.com/upload/storage/, a version v1/, more static structure b/, a data element {bucket}/ and yet more static structure o. Thus code has to be written for each endpoint that correctly cobbles together the complete path from a combination of elements that will never change (static structure), might sometimes change (version) and will always change (data).
Of course that's nothing too daunting with some string.formats and plenty of UrlEscaping, but I've always found it distracting, repetitive and doesn't follow the normal programming idioms we are used to. A rest endpoint is just another method call, albeit one who's implementation is well separated from the caller's process.
The DLR gives us a way to mix the static and dynamic parts of a rest invocation in a way that feels more like local method invocations.
We'll write a short console application that uses the Dynamic Rest Client to enumerate all of the buckets and their items in a Google Cloud Storage project. It uses a couple helper classes to deal with authorization and storage of the auth tokens, but the essence of the program is only a handful of code.
First just an entry point, asking the user for a storage project name and calling a task:
static void Main(string[] args)
{
Console.WriteLine("What is the name of the storage project?");
var project = Console.ReadLine();
var t = EnumerateObjects(project);
t.Wait();
}
And then authenticate the user and authorize the app to access their cloud storage account. As an aside the Google Oauth2 code uses the device authorization path as it is the simplest to use when you don't have access to an integrated web browser.
var auth = new GoogleOAuth2("https://www.googleapis.com/auth/devstorage.read_write");
var token = await auth.Authenticate("");
Endpoint paths are specified as part of a call chain of methods and properties off of the root proxy instance. Given this instantiation:
dynamic google = new DynamicRestClient("https://www.googleapis.com/");
all subsequent calls to the object google will be rooted at https://www.googleapis.com/. Any property access, method call or delegate invocation to that root instance that is not an http verb invocation (get, put, post, delete) will return a dynamic object relative to the root instance which cans be chained together to form a complete path.
So if we wanted to get to the version 1 of the bucket upload storage API we could do this:
dynamic bucketEndPoint = google.upload.storage.v1.b;
Version number aside, all of the elements in this path are static structure and can compiled into our code. Any change to that endpoint would likely require a recompile anyway.
The bucket identifier will likely never be hard coded. It is data that will be determined at runtime 99% of the time. We'd get the bucket typically by asking the user for a selection or as a function of some other part of a larger program but if we just want to enumerate all
dynamic buckets = await google.storage.v1.b.get(project: project);
Since we now have a dynamic object with the enumeration of all of the buckets we can enumerate the buckets, and the object stored in each:
foreach (var bucket in buckets.items)
{
Console.WriteLine("bucket {0}: {1}", bucket.id, bucket.name);
dynamic contents = await bucketEndPoint(bucket.id).o.get();
foreach (var item in contents.items)
{
Console.WriteLine("\tid: {0}", item.id);
Console.WriteLine("\tname: {0}", item.name);
Console.WriteLine("\tcontent type: {0}", item.contentType);
Console.WriteLine("\t-----");
}
}
The entire program looks like this, enabling easy access to a REST api with no static DTO types or wrapper classes:
using System;
using System.Threading.Tasks;
using DynamicRestProxy.PortableHttpClient;
using UnitTestHelpers;
namespace GoogleStorageConsole
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("What is the name of the storage project?");
var project = Console.ReadLine();
var t = EnumerateObjects(project);
t.Wait();
}
private static async Task EnumerateObjects(string project)
{
var auth = new GoogleOAuth2("https://www.googleapis.com/auth/devstorage.read_write");
var token = await auth.Authenticate("");
var defaults = new DynamicRestClientDefaults()
{
AuthScheme = "OAuth",
AuthToken = token
};
dynamic google = new DynamicRestClient("https://www.googleapis.com/", defaults);
dynamic bucketEndPoint = google.storage.v1.b;
dynamic buckets = await bucketEndPoint.get(project: project);
foreach (var bucket in buckets.items)
{
Console.WriteLine("bucket {0}: {1}", bucket.id, bucket.name);
dynamic contents = await bucketEndPoint(bucket.id).o.get();
foreach (var item in contents.items)
{
Console.WriteLine("\tid: {0}", item.id);
Console.WriteLine("\tname: {0}", item.name);
Console.WriteLine("\tcontent type: {0}", item.contentType);
Console.WriteLine("\t-----");
}
}
}
}
}