
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<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 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<string, string> { {"query", "GetAsync"}, {"sort_order", "desc"}}) .AddParameter("baseUrl", "http://api.github.com") .AddParameter("folder", new List<string> {"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 <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 Application Controller Process Request Client View Client State Model HTTP Response Machine 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",
Details
-
File Typepdf
-
Upload Time-
-
Content LanguagesEnglish
-
Upload UserAnonymous/Not logged-in
-
File Pages49 Page
-
File Size-