BUILDING API CLIENT LIBRARIES THAT DON’T SUCK

@darrel_miller Code Monkey TODAY’S PLAN

Foundations Why build them? Structure

HTTP Client URIs Requests/Responses Managing State

Decor Services, Missions and Reactive UI WHY CLIENT LIBRARIES?

Reduce Developer Effort

Improve Client Application Quality Improve Client Application Performance

Increase Developer Adoption HTTP CLIENT – COMMON INTERFACE

var response = await httpClient.GetAsync("http://example.org");

var response = await httpClient.PutAsync("http://example.org", content);

var response = await httpClient.PostAsync("http://example.org", content);

var response = await httpClient.DeleteAsync("http://example.org", content);

var response = await httpClient.SendAsync(request);

HttpMethod.Head HttpMethod.Options HTTP CLIENT - LIFETIME

public async Task GetAllThings() { using (HttpClient httpClient = new HttpClient()) { var uri = "http://example.org/api/search?key=..."; var response = await httpClient.GetAsync(uri); var jdata = await response.Content.ReadAsStringAsync(); return jdata; } } HTTP CLIENT - LIFETIME

public class ApiService { private HttpClient _HttpClient;

public ApiService() { _HttpClient = new HttpClient(new WebRequestHandler());

_HttpClient.DefaultRequestHeaders.UserAgent.Add( new ProductInfoHeaderValue("ApiService", "1.0"));

_HttpClient.DefaultRequestHeaders.AcceptLanguage.Add( new StringWithQualityHeaderValue("en")); } } HTTP CLIENT STATE

BaseAddress Timeout MaxResponseContentBufferSize

DefaultRequestHeaders

PendingRequestsCancellationTokenSource HTTP CLIENT - MIDDLEWARE

Client Application

HttpClient

MessageHandler Caching Logging Redirection Compression Cookie Handling MessageHandler Authorization

HttpClientHandler HTTP Handler WebRequestHandler WinHttpHandler NativeMessageHandler HTTP CLIENT - FACTORY

var client = HttpClientFactory.Create(new[] { new AuthMiddleware(), new CachingMiddleware(), new RetryMiddleware() });

https://www.nuget.org/packages/Microsoft.AspNet.WebApi.Client/ HTTP CLIENT - BUILDER

var builder = new ClientBuilder();

builder.Use(new AuthMiddleware(...)); builder.Use(new CachingMiddleware(...)); builder.Use(new RetryMiddleware(...));

var client = builder.Build(); HTTP CLIENT - AUTHENTICATION

client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", "sometoken"); HTTP CLIENT - AUTHENTICATION

var builder = new ClientBuilder();

var cache = new HttpCredentialCache(); LoadCacheWithCredentials(cache); builder.UseAuthHandler(cache);

var client = builder.Build(); HTTP CLIENT - AUTHENTICATION

Unauthorized Request 401 with WWW-Authenticate Header Auth User-agent Origin Server Middleware Authorized Request 200 OK

Credential Cache HTTP CLIENT - CACHING

User Agent

Private Cache Corporate Proxy Cache

ISP Cache

Reverse Proxy Cache

Output Cache

Origin Server WININET PROXY CACHE

IE, Chrome WebClient HttpClient

WebRequestHandler WinHttpHandler

HttpWebRequest

In a Service

WinInet WinHttp

Client Cache No Client Cache HTTPCLIENT

var builder = new ClientBuilder();

builder.UseAuthHandler(GetCredentialCache(creds));

builder.UseHttpCache(new InMemoryContentStore());

var client = builder.Build(); HTTPCLIENT - CACHING

Request HttpCacheHandler

Response

QueryCacheResult (ReturnStored | Revalidate | HttpResponseMessage CannotUseCache)

HttpCache Stored Response

IContentStore CONDITIONAL REQUESTS

User Private GET Origin Agent Cache 200 Server Fresh CacheControl : Max-age=5 Response Etag : xyz

User GET Private Origin Agent 200 Cache Server Fresh Response

If-None-Match: xyz User GET Private GET Origin Agent 200 Cache 304 Server FreshStale CacheControl : Max-age=5 Response RESOURCE IDENTIFIERS

https://example.org/my-resource

Discover Resolve Dereference URL CONSTRUCTION

Don’t write custom URL construction code URI TEMPLATES

Consider using URI Templates RFC 6570

Github does! EXTREME TEMPLATES

var url = new UriTemplate("{+baseUrl}{/folder*}/search/code{/language}{?params*}")

.AddParameter("params", new Dictionary { {"query", "GetAsync"}, {"sort_order", "desc"}}) .AddParameter("baseUrl", "http://api.github.com") .AddParameter("folder", new List {"home", "src", "widgets"})

.Resolve();

http://api.github.com/home/src/widgets/search/code?query=GetAsync&sort_order=desc

http://www.bizcoder.com/constructing-urls-the-easy-way MAKE CHANGE EASY

Centralize Hardcoded URLs LINK TYPES

LINKS ARE DATA

{ "swagger": "2.0", "info": { "title": "Forecast API", "description": "The Forecast API lets you query for most locations on the globe, and returns: current conditions, minute-by-minute forecasts out to 1 hour (where available), hour- by-hour forecasts out to 48 hours, and more.", "version": "1.0" }, "host": "api.forecast.io", "basePath": "", "schemes": [ "https" ], "paths": { "/forecast/{apiKey}/{latitude},{longitude}": { "get": {...} } } } LINK – DISCOVERY DOCUMENT

var discoverLink = new OpenApiLink() { Target = new Uri("http://api.forecast.io/") }; var discoveryResponse = await client.SendAsync(discoverLink.CreateRequest());

_discoveryDoc = OpenApiDocument.Load(await discoveryResponse.Content.ReadAsStreamAsync());

var forecastLink = discoveryDoc.GetOperationLink("getForecast"); var forecastResponse = await client.SendAsync(speakersLink.CreateRequest()); BREAKING REQUEST FROM RESPONSE

Create Request

Process Request

Handle Response CREATING REQUESTS

var link = new SpeakersLink {SpeakerName = "bob"}; var request = link.CreateRequest();

var response = await _httpClient.SendAsync(request); REQUEST FACTORY

public interface IRequestFactory { string LinkRelation { get; } HttpRequestMessage CreateRequest(); } FOLLOW LINK

var link = new SpeakersLink {SpeakerName = "bob"};

var response = await _httpClient.FollowLinkAsync(request); FOLLOW SECRETS

public static class HttpClientExtensions { public const string PropertyKeyRequestFactory = "tavis.requestfactory";

public static Task FollowLinkAsync( this HttpClient httpClient, IRequestFactory requestFactory, IResponseHandler handler = null) { var httpRequestMessage = requestFactory.CreateRequest(); httpRequestMessage.Properties[PropertyKeyRequestFactory] = requestFactory;

return httpClient.SendAsync(httpRequestMessage) .ApplyRepresentationToAsync(handler); } } CENTRALIZED RESPONSE HANDLING

Create Request

Process Request

HTTP Response Machine FIRE AND FORGET

var machine = GetHttpResponseMachine();

await httpClient.FollowLinkAsync(link,machine); DISPATCH ON STATUS

bool ok = false; var machine = new HttpResponseMachine();

machine.When(HttpStatusCode.OK) .Then(async (l, r) => { ok = true; });

await machine.HandleResponseAsync(null, new HttpResponseMessage(HttpStatusCode.OK));

Assert.True(ok); DISPATCH ON MEDIA TYPE

machine.When(HttpStatusCode.OK, null, new MediaTypeHeaderValue("application/json")) .Then(async (l, r) => { var text = await r.Content.ReadAsStringAsync(); root = JToken.Parse(text); });

machine.When(HttpStatusCode.OK, null, new MediaTypeHeaderValue("application/xml")) .Then(async (l, r) => { ... }); NEED MORE CONTEXT machine.When(HttpStatusCode.OK, linkRelation: "http://tavis.net/rels/login") .Then(LoginSuccessful); machine.When(HttpStatusCode.Unauthorized, linkRelation:"http://tavis.net/rels/login ") .Then(LoginFailed); machine.When(HttpStatusCode.Forbidden, linkRelation: "http://tavis.net/rels/login") .Then(LoginForbidden); machine.When(HttpStatusCode.BadRequest, linkRelation: "http://tavis.net/rels/login") .Then(FailedRequest); machine.When(HttpStatusCode.OK, linkRelation: " http://tavis.net/rels/reset") .Then(ResetForm); PARSING MESSAGES

machine .When(HttpStatusCode.OK) .Then((m, l, p) => { aPerson = p; }); PARSING MESSAGES

var parserStore = new ParserStore(); parserStore.AddMediaTypeParser("application/json", async (content) => { var stream = await content.ReadAsStreamAsync(); return JToken.Load(new JsonTextReader(new StreamReader(stream))); }); parserStore.AddLinkRelationParser(“http://tavis.net/rels/person", (jt) => { var person = new Person(); var jobject = (JObject)jt; person.FirstName = (string)jobject["FirstName"]; person.LastName = (string)jobject["LastName"]; return person; }); var machine = new HttpResponseMachine(parserStore); STATE MANAGEMENT

Create Request

Application Controller

Process Request Client View

Client State Model HTTP Response Machine AFFECT MY STATE

Model test = new Model(); var parserStore = LoadParserStore(); var machine = new HttpResponseMachine>(test,parserStore); machine.When(HttpStatusCode.OK) .Then((m, l, p) => { m.Value = p; }); DEATH TO SERIALIZERS

Missing values, unrecognized properties Does null mean unspecified, or explicitly null? Non standard data types: datetime, timespan Empty collection or no collection Capitalization Links Cycles Changes to behavior in the frameworks Security Risks STREAMING JSON PARSER

public static void ParseStream(Stream stream, object rootSubject, VocabTerm rootTerm) { using (var reader = new JsonTextReader(new StreamReader(stream))) { ...

while (reader.Read()) { switch (reader.TokenType) { ... } } } } PARSING VOCABULARIES

var vocab = new VocabTerm();

vocab.MapProperty("type", (s, o) => s.ProblemType = new Uri(o)); vocab.MapProperty("title", (s, o) => s.Title = o); vocab.MapProperty("detail", (s, o) => s.Detail = o); vocab.MapProperty("instance", (s, o) => { s.ProblemInstance = new Uri(o,UriKind.RelativeOrAbsolute); });

var problem = new ProblemDocument();

JsonStreamingParser.ParseStream(stream, problem, vocab); PARTIAL PARSING

var opsTerm = new VocabTerm();

var pathTerm = new VocabTerm(); pathTerm.MapAnyObject(opsTerm, (s, p) => { return s.AddOperation(p, Guid.NewGuid().ToString()); });

var pathsTerm = new VocabTerm("paths");

pathsTerm.MapAnyObject(pathTerm,(s,p) => s.AddPath(p));

var rootTerm = new VocabTerm(); rootTerm.MapObject(pathsTerm, (s) => s);

var openAPI = new OpenApiDocument(); JsonStreamingParser.ParseStream(stream, openAPI, rootTerm); ASSEMBLING THE PIECES

HttpClient

Client Application Middleware State

Application Links Response Machine Parsers SUMMARY

Leverage the developer’s Use HTTP’s layered Own your message knowledge of HTTP and architecture to add parsing their platform value

Allow HTTP’s uniform Make asynchrony work for interface to enable React don’t assume you reuse LIBRARIES

http://github.com/tavis-software Tavis.UriTemplates Tavis.HttpCache Tavis.Link Tavis.Auth Tavis.Home Tavis.JsonPointer Tavis.Problem Tavis.Hal Tavis.JsonPatch Tavis.Status

http://hapikit.github.io Tooling for building Hapikit.net better HTTP API Client Hapikit.py Libraries Hapikit.go … SAMPLES

https://github.com/Runscope/dotnet-webpack

https://github.com/hapikit/github.net.hapikit

https://github.com/darrelmiller/ForceLinksForNet

https://github.com/hapikit/stormpath.net.hapikit

https://github.com/hapikit/haveibeenpwnd.net.hapikit BON APPETIT

Twitter: @darrel_miller Blog: http://www.bizcoder.com/ Latest Slides: http://bit.ly/darrel-sddconf