BUILDING API CLIENT LIBRARIES - SDD Conference...TODAY’S PLAN Why build them? Foundations...
Transcript of BUILDING API CLIENT LIBRARIES - SDD Conference...TODAY’S PLAN Why build them? Foundations...
BUILDING API CLIENT
LIBRARIESTHAT DON’T SUCK
@darrel_miller
Code Monkey
TODAY’S PLAN
Why build them?
Foundations
Structure
Decor
HTTP Client Library
URIs
Requests/Responses
Managing State
Services, Missions and Reactive UI
WHY CLIENT LIBRARIES?
Increase Developer Adoption
Reduce Developer Effort
Improve Client Application Quality
Improve Client Application Performance
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.HeadHttpMethod.Options
public async Task<string> 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 - LIFETIME
HTTP CLIENT STATE
BaseAddress
Timeout
MaxResponseContentBufferSize
DefaultRequestHeaders
PendingRequestsCancellationTokenSource
HTTP CLIENT - MIDDLEWARE
Client Application
HttpClient
MessageHandler
MessageHandler
HTTP HandlerHttpClientHandler
WebRequestHandler
WinHttpHandler
NativeMessageHandler
CachingLogging
RedirectionCompression
Cookie HandlingAuthorization
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
User-agent Origin Server
Unauthorized Request
401 with WWW-Authenticate Header
Authorized Request
200 OK
Auth
Middleware
Credential
Cache
HTTP CLIENT - CACHING
User Agent
Origin Server
Output Cache
Reverse Proxy Cache
ISP Cache
Corporate Proxy Cache
Private Cache
WININET PROXY CACHE
HttpClient
WebRequestHandler
HttpWebRequest
WinHttpHandler
WinInet WinHttp
IE, Chrome WebClient
In a Service
Client Cache No Client Cache
HTTPCLIENT
var builder = new ClientBuilder();
builder.UseAuthHandler(GetCredentialCache(creds));
builder.UseHttpCache(new InMemoryContentStore());
var client = builder.Build();
HTTPCLIENT - CACHING
Stored Response
HttpCacheHandler
HttpCache
IContentStore
QueryCacheResult(ReturnStored | Revalidate |
CannotUseCache)HttpResponseMessage
Request
Response
CONDITIONAL REQUESTS
User
Agent
Origin
Server
Private
CacheFresh
Response
User
Agent
Origin
ServerPrivate
CacheFresh
Response
GET
200
CacheControl : Max-age=5
Etag : xyz
GET
200
User
Agent
Origin
Server
Private
Cache
GET
200
GET
304
If-None-Match: xyz
CacheControl : Max-age=5Stale
Response
Fresh
Response
RESOURCE IDENTIFIERS
Discover Resolve Dereference
https://example.org/my-resource
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<string, string> { {"query", "GetAsync"},
{"sort_order", "desc"}}).AddParameter("baseUrl", "http://api.github.com").AddParameter("folder", new List<string> {"home", "src", "widgets"})
.Resolve();
http://www.bizcoder.com/constructing-urls-the-easy-way
http://api.github.com/home/src/widgets/search/code?query=GetAsync&sort_order=desc
MAKE CHANGE EASY
Centralize Hardcoded URLs
LINK TYPES
<link rel="apple-touch-icon image_src“href="//cdn.sstatic.net/Sites/stackoverflow/img/apple-touch-icon.png?v=...">
<link rel="search" type="application/opensearchdescription+xml" title="Stack Overflow" href="/opensearch.xml">
<link rel="stylesheet" type="text/css" href="//cdn.sstatic.net/Sites/stackoverflow/all.css?v=3f4c51969762">
<link rel="alternate“type="application/atom+xml" title="Feed of recent questions" href="/feeds">
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<HttpResponseMessage> 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<Person>((m, l, p) => { aPerson = p; });
PARSING MESSAGES
var parserStore = new ParserStore();
parserStore.AddMediaTypeParser<JToken>("application/json", async (content) =>{
var stream = await content.ReadAsStreamAsync();return JToken.Load(new JsonTextReader(new StreamReader(stream)));
});
parserStore.AddLinkRelationParser<JToken, Person>(“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
Process Request
HTTP Response
Machine
Client State
Model
Client View
Application
Controller
AFFECT MY STATE
Model<Person> test = new Model<Person>();
var parserStore = LoadParserStore();
var machine = new HttpResponseMachine<Model<Person>>(test,parserStore);
machine.When(HttpStatusCode.OK).Then<Person>((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<ProblemDocument>();
vocab.MapProperty<string>("type", (s, o) => s.ProblemType = new Uri(o));vocab.MapProperty<string>("title", (s, o) => s.Title = o);vocab.MapProperty<string>("detail", (s, o) => s.Detail = o);vocab.MapProperty<string>("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<Operation>();
var pathTerm = new VocabTerm<Path>();pathTerm.MapAnyObject(opsTerm, (s, p) => {
return s.AddOperation(p, Guid.NewGuid().ToString());});
var pathsTerm = new VocabTerm<OpenApiDocument>("paths");
pathsTerm.MapAnyObject(pathTerm,(s,p) => s.AddPath(p));
var rootTerm = new VocabTerm<OpenApiDocument>();rootTerm.MapObject(pathsTerm, (s) => s);
var openAPI = new OpenApiDocument();JsonStreamingParser.ParseStream(stream, openAPI, rootTerm);
ASSEMBLING THE PIECES
HttpClient
Application Links Response Machine
Client Application
StateMiddleware
Parsers
SUMMARY
Leverage the developer’s
knowledge of HTTP and
their platform
Use HTTP’s layered
architecture to add
value
Own your message
parsing
Make asynchrony work for
you
Allow HTTP’s uniform
interface to enable
reuse
React don’t assume
LIBRARIES
http://github.com/tavis-software
Tavis.UriTemplates
Tavis.Link
Tavis.Home
Tavis.Problem
Tavis.JsonPatch
Tavis.HttpCache
Tavis.Auth
Tavis.JsonPointer
Tavis.Hal
Tavis.Status
http://hapikit.github.io
Hapikit.net
Hapikit.py
Hapikit.go
…
Tooling for building
better HTTP API Client
Libraries
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