Monday, January 22, 2018

Microsoft 70-487: Consume Web API web services

Exam Objectives

Consume Web API services by using HttpClient synchronously and asynchronously; send and receive requests in different formats (JSON/HTML/etc.); request batching


Quick Overview of Training Materials




Consume services with HttpClient


I'm going to go out on a limb and assume that the version of HttpClient we care about here is the one from the System.Net.Http namespace, NOT the one from the Windows.Web.Http namespace (which appears to be intended for UWP and only works on .NET 4.5+). The demos in "Consuming the API" section of the "Building and Securing a RESTful API for Multiple Clients in ASP.NET" PluralSight course use this client, so I'm gonna roll with that. The Exam Ref also assumes this class (though he doesn't come out and say it).  There are extension methods for the HttpClient class used by the Exam Ref, which are part of the HttpClientExtensions class.

The Calling a Web API From a .NET Client (C#) documentation walks through how to use an HttpClient instance to interact with a RESTful API.  I didn't feel like reproducing the server side api just for the sake of implementing an HttpClient demo, so I'm using the simple open api provided at https://dog.ceo/dog-api/.  This is a read-only API, so I can just do GET requests, but I think it will get the point across.

The DogApiClient class implements one API method: get all breeds.  The part played by HttpClient is pretty minimal; most of the interesting code is dealing with the deserialization with the Newtonsoft.Json libraries:


    public class DogApiClient
    {
        private static HttpClient client = new HttpClient();
        private const string baseUrl = "https://dog.ceo/api/";

        public async Task<IEnumerable<string>> GetBreedsListAsync()
        {
            IEnumerable<string> list = null;
            string path = baseUrl + "breeds/list/all";
            HttpResponseMessage response = await client.GetAsync(path);

            var definition = new { status = "", message = new Dictionary<string,List<string>>() };

            if (response.IsSuccessStatusCode)
            {
                string rawJson = await response.Content.ReadAsStringAsync();
                list = JsonConvert.DeserializeAnonymousType(rawJson, definition)
                       .message
                       .SelectMany(b => b.Value.DefaultIfEmpty()
                       .Select(s => b.Key + " " + s));
            }
            return list;
        }

        //same method without all the async-iness...
        public IEnumerable<string> GetBreedsList()
        {
            IEnumerable<string> list = null;
            string path = baseUrl + "breeds/list/all";
            HttpResponseMessage response = client.GetAsync(path).Result;

            var definition = new { status = "", message = new Dictionary<string, List<string>>() };

            if (response.IsSuccessStatusCode)
            {
                string rawJson = response.Content.ReadAsStringAsync().Result;
                list = JsonConvert.DeserializeAnonymousType(rawJson, definition)
                       .message
                       .SelectMany(b => b.Value.DefaultIfEmpty()
                       .Select(s => b.Key + " " + s));
            }
            return list;
        }

        //if we wanted to be super lazy we could do it this way...
        public IEnumerable<string> GetBreedsList2()
        {
            return GetBreedsListAsync().Result;
        }
    }


The simple console app just calls this method and prints out all the dog breeds:



Major breeds get a line for each subbreed because of the linq expression SelectMany (credit to StackOverflow for that one...), and the anonymous object deserialization approach was in the Newtonsoft documentation.

The CodeMaze article gives a little bit of background on alternative approaches to using HttpClient to make web requests (as well as HttpClient itself, though the author links to a Channel9 presentation on the UWP version, probably a bit of confusion...).  HttpWebRequest is a lower level abstraction for making requests that gives you much finer control over how the request is executed. This reminded me of clients I'd written in Java using URLConnection... anyway...

While it wasn't necessary for me to fiddle with the client too much, one common need when dealing with such a client is adding additional request headers.  The client object has a property called DefaultRequestHeaders, with an Add method that takes two strings, effectively adding a header to the collection.  The HttpClient class has methods for sending GET, POST, PUT, and DELETE, it also includes a SendAsync() method which takes an HttpRequestMessage object.  This object can be used basically like a configuration object, allowing you to set headers or alternate methods (PATCH) for a single request (which would be a better approach to take when using a single static HttpClient object).

One final thing worthy of note regarding the HttpClient class is that using it incorrectly can lead to resource leaks.  This is detailed in the ASP.NET Monsters article.  The pattern he'd been using that caused him trouble was to instantiate a new HttpClient in a using block.  Even though this was disposing of the instance, it was still sucking up all the available sockets.  The simple solution was to reuse a single static instance of the HttpClient class for all requests.  The article is interesting and worth a look, especially the comments which point out some of the weaknesses of this approach (caching, load balancing) and potential alternative approaches.



Send and Receive in different formats


Yeeeeah I think the Design a Web API (Choose appropriate format) and Implement a Web API (Accept JSON data, Content Negotiation) posts cover this for the most part.  The important thing to remember is that you need to use the Accept header from the client to specify which media formats you want to accept, and use the Content-Type header to tell the server what kind of data you are sending when posting data back to the server.  If the data you want to deal with is not included by default, you'll need to implement a custom MediaFormatter (see the Design post for an example).



Request Batching


The blog post ASP.NET Web API Request Batching demonstrates how to do request batching in Web API (though on second inspection it seems it's probably mostly lifted from the documentation, which actually has a more varied example...).  Using the batching functionality requires a simple change on the server, and special treatment on the client.  In the server, it is only necessary to map a special batching route:


    //Add batch route
    config.Routes.MapHttpBatchRoute(
        routeName: "batch",
        routeTemplate: "api/batch",
        batchHandler: new CustomHttpBatchHandler(GlobalConfiguration.DefaultServer)
    );


One thing to watch out for that tripped me up:  I initially defined this route after the super generic default route, and as a result all my requests to this were being intercepted and forwarded to a nonexistent "batch" controller, resulting in a 404 response.  So don't do that...

Most of the complication with this approach falls to the client.  The examples follow this basic template:
  • requests are defined as HttpRequestMessage objects, complete with method, url, headers, etc.
  • these requests are passed into a new HttpMessageContent object
  • a MultipartContent object is created, and all the HttpMessageContent objects added to it
  • the MultipartContent object is set as the content to a new HttpRequestMessage, with a method of POST and the url set to the batch endpoint
  • HttpClient is used to send this message
  • a MultipartMemoryStreamProvider is used to pull out the individual responses from the Content property on the response



       static void Main(string[] args)
        {
            HttpClient client = new HttpClient();

            //create the requests to be batched
            string baseUrl = "http://localhost:34245/api/Dogs/";
            var getDog1 = new HttpRequestMessage(HttpMethod.Get, baseUrl + "1");
            var getDog3 = new HttpRequestMessage(HttpMethod.Get, baseUrl + "3");
            var getGizmo = new HttpRequestMessage(HttpMethod.Get, baseUrl + "Gizmo");
            getGizmo.Headers.Add("Accept", @"application/Dog");

            //create the batch container
            MultipartContent content = 
                new MultipartContent("mixed", "batch_" + Guid.NewGuid().ToString());
            content.Add(new HttpMessageContent(getDog1));
            content.Add(new HttpMessageContent(getDog3));
            content.Add(new HttpMessageContent(getGizmo));

            //create the batch request
            HttpRequestMessage batchRequest = 
                new HttpRequestMessage(HttpMethod.Post, "http://localhost:34245/api/batch");
            batchRequest.Content = content;

            //send the batch request with the client and parse the response
            HttpResponseMessage response = client.SendAsync(batchRequest).Result;
            MultipartMemoryStreamProvider responseContents = 
                response.Content.ReadAsMultipartAsync().Result;

            foreach(var c in responseContents.Contents)
            {
                Console.WriteLine(c.ReadAsStringAsync().Result + "\n\n");
            }

            Console.ReadLine();
        }




Playing around with Fiddler we can see what the batch request is actually doing:



Each part of the multipart message contains the information necessary for Web API to route that request the way it otherwise would have.  This stack overflow question also deals with batching, though I didn't experience his particular problem (I just needed to do the routing in the right order).


No comments:

Post a Comment