BUILDING API CLIENT LIBRARIES THAT DON’T SUCK
@darrel_miller Code Monkey TODAY’S PLAN
Foundations Why build them? Structure
HTTP Client Library 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
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
.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
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
var parserStore = new ParserStore(); parserStore.AddMediaTypeParser
Create Request
Application Controller
Process Request Client View
Client State Model HTTP Response Machine AFFECT MY STATE
Model
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
var problem = new ProblemDocument();
JsonStreamingParser.ParseStream(stream, problem, vocab); PARTIAL PARSING
var opsTerm = new VocabTerm
var pathTerm = new VocabTerm
var pathsTerm = new VocabTerm
pathsTerm.MapAnyObject(pathTerm,(s,p) => s.AddPath(p));
var rootTerm = new VocabTerm
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