That Don't Suck

That Don't Suck

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",

View Full Text

Details

  • File Type
    pdf
  • Upload Time
    -
  • Content Languages
    English
  • Upload User
    Anonymous/Not logged-in
  • File Pages
    49 Page
  • File Size
    -

Download

Channel Download Status
Express Download Enable

Copyright

We respect the copyrights and intellectual property rights of all users. All uploaded documents are either original works of the uploader or authorized works of the rightful owners.

  • Not to be reproduced or distributed without explicit permission.
  • Not used for commercial purposes outside of approved use cases.
  • Not used to infringe on the rights of the original creators.
  • If you believe any content infringes your copyright, please contact us immediately.

Support

For help with questions, suggestions, or problems, please contact us