Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feedback On Strongly Typed HttpClients #1

Open
TehWardy opened this issue May 18, 2018 · 0 comments
Open

Feedback On Strongly Typed HttpClients #1

TehWardy opened this issue May 18, 2018 · 0 comments

Comments

@TehWardy
Copy link

Hey man, I realise i'm late to the show a bit here but I was just watching this on youtube ...
https://www.youtube.com/watch?v=Lb12ZtlyMPg&index=15&list=PL1rZQsJPBU2StolNg0aqvQswETPcYnNKL

...
I use WebAPI OData extensively and here's how I took HttpClient and extended it to get the desired functionality ...

using log4net;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Configuration;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
using System.Web;
using System.Web.Configuration;
using System.Web.SessionState;

namespace Core.Objects
{
    public static class HttpClientExtensions
    {
        static readonly ILog log = LogManager.GetLogger(typeof(HttpClient));


        /// <summary>
        /// Signs and sends a http request
        /// </summary>
        /// <typeparam name="T">type of data being sent</typeparam>
        /// <param name="client">client to use to send</param>
        /// <param name="query">where to send it</param>
        /// <param name="data">object to send</param>
        /// <param name="signature">signature</param>
        /// <returns></returns>
        public static async Task<TResult> PostAsJsonAsync<T, TResult>(this HttpClient client, string query, T data, string signature = null)
        {
            if (signature != null) client.DefaultRequestHeaders.Add("signature", signature);
            var result = await client.PostAsJsonAsync(query, data);
            if (signature != null) client.DefaultRequestHeaders.Remove("signature");
            return await result.Content.ReadAsAsync<TResult>();
        }

        public static async Task<ICollection<TResult>> PostAndRetrieveODataCollectionAsync<T, TResult>(this HttpClient client, string query, T data, string signature = null)
        {
            var result = await client.PostAsJsonAsync<T, ODataCollection<TResult>>(query, data, signature);

            return result.Value;
        }

        public static async Task<T> Get<T>(this HttpClient client, string query)
        {
            HttpResponseMessage response = null;
            try
            {
                response = await client.GetAsync(query);
                return await response.Content.ReadAsAsync<T>();
            }
            catch (Exception ex)
            {
                log.Error("Problem querying " + query);
                log.Error(ex);
                if (response != null)
                {
                    var apiException = await response.Content.ReadAsStringAsync();
                    var err = new Exception(apiException, ex);
                    log.Error(err);
                    throw err;
                }
                throw ex;
            }
        }

        public static async Task<T> GetFromOData<T>(this HttpClient client, string query)
        {
            HttpResponseMessage response = null;
            try
            {
                response = await client.GetAsync(query);
                var result = await response.Content.ReadAsAsync<ODataResult<T>>();
                return result.Value;
            }
            catch (Exception ex)
            {
                log.Error("Problem querying " + query);
                log.Error(ex);
                if (response != null)
                {
                    var apiException = await response.Content.ReadAsStringAsync();
                    var err = new Exception(apiException, ex);
                    log.Error(err);
                    throw err;
                }
                throw ex;
            }
        }
        
        public static async Task<ICollection<T>> GetODataCollection<T>(this HttpClient client, string query)
        {
            try
            {
                return (
                    await client.GetAsync(query)
                        .ContinueWith(t => t.Result.Content.ReadAsAsync<ODataCollection<T>>())
                        .Unwrap()
                    )
                    .Value;
            }
            catch (Exception)
            {
                log.Error("Problem querying " + query);
                throw;
            }
        }
       
        /// <summary>
        /// Adds authorization information to the client by making an auth call with the given credentials
        /// </summary>
        /// <param name="client">The HttpClient to attach the authorization information to</param>
        /// <param name="user">The username to use for authentication</param>
        /// <param name="pass">The password to use for authentication</param>
        /// <returns>An authenticated HttpClient</returns>
        public static async Task<JObject> Authenticate(this HttpClient client, string user, string pass)
        {
            var authRequest = await client.PostAsync("Authenticate", new StringContent("username=" + user + "&password=" + pass + "&grant_type=password"));
            var authResponse = await authRequest.Content.ReadAsStringAsync();

            if (!authResponse.StartsWith("<!DOCTYPE"))
            {
                dynamic token = JObject.Parse(authResponse);

                try {
                    client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(token.token_type.ToString(), token.access_token.ToString());
                    return token;
                }
                catch { /* if we get here the server returned a json repsonse but it wasn't a token, it was more than likely an auth failure. */ }
            }
            else log.Warn("Auth request Failed for user " + user);
            
            return null;
        }

        /// <summary>
        /// Adds the given collection of header values to an instance of a http client
        /// </summary>
        /// <param name="client">the http client</param>
        /// <param name="headers">the header values to add</param>
        /// <returns>HttpClient with the given header values</returns>
        public static HttpClient AddHeaders(this HttpClient client, NameValueCollection headers)
        {
            foreach (var key in headers.Keys)
                try { client.DefaultRequestHeaders.Add(key.ToString(), headers.Get(key.ToString())); } catch { }

            return client;
        }

        public static HttpClient UseBasicAuth(this HttpClient client, string user, string pass)
        {
            return client.UseBasicAuth(Convert.ToBase64String(Encoding.UTF8.GetBytes("username=" + user + "&password=" + pass + "&grant_type=password")));
        }

        public static HttpClient UseBasicAuth(this HttpClient client, string authString)
        {
            client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("basic", authString);
            return client;
        }

        public static HttpClient UseBaseUrl(this HttpClient client, string url)
        {
            client.BaseAddress = new Uri(url);
            return client;
        }

        /// <summary>
        /// Adds authorization information from the given session to the given HttpClient
        /// </summary>
        /// <param name="client">The HttpClient to attach the authorization information to</param>
        /// <param name="request">The HttpSessionState from which to acquire the authorization information</param>
        /// <returns>An authenticated HttpClient</returns>
        public static HttpClient AddAuthFromSession(this HttpClient client, HttpSessionStateBase session)
        {
            if (client.DefaultRequestHeaders.Authorization == null && session != null)
            {
                var token = (JObject)session["token"];
                if(token != null)
                    client.DefaultRequestHeaders.Add("Authorization", "bearer " + token["access_token"].ToString());
            }
            return client;
        }

        /// <summary>
        /// Adds authorization information from the given session to the given HttpClient
        /// </summary>
        /// <param name="client">The HttpClient to attach the authorization information to</param>
        /// <param name="request">The HttpSessionState from which to acquire the authorization information</param>
        /// <returns>An authenticated HttpClient</returns>
        public static HttpClient AddAuthFromSession(this HttpClient client, HttpSessionState session)
        {
            if(client.DefaultRequestHeaders.Authorization == null && session != null)
            {
                var token = (JObject)session["token"];
                if (token != null)
                    client.DefaultRequestHeaders.Add("Authorization", "bearer " + token["access_token"].ToString());
            }
            return client;
        }

        /// <summary>
        /// Adds authorization information from the given request to the given HttpClient
        /// </summary>
        /// <param name="client">The HttpClient to attach the authorization information to</param>
        /// <param name="request">The HttpRequest from which to acquire the authorization information</param>
        /// <returns>An authenticated HttpClient</returns>
        public static HttpClient AddAuthFromRequest(this HttpClient client, HttpRequest request)
        {
            var auth = request.Headers["authorization"];
            if(auth != null && client.DefaultRequestHeaders.Authorization == null) client.DefaultRequestHeaders.Add("Authorization", auth);
            return client;
        }

        /// <summary>
        /// Sets the base URI on the given HttpClient instance to the one in config
        /// </summary>
        /// <param name="client">the HttpClient</param>
        /// <returns>The HttpClient (updated)</returns>
        public static HttpClient WithApiBaseUriFromConfig(this HttpClient client)
        {
            client.BaseAddress = new Uri(ConfigurationManager.AppSettings["ApiUrl"]);
            return client;
        }

        public static HttpClient AddAuthToken(this HttpClient client, string token)
        {
            client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", token);
            return client;
        }

        /// <summary>
        /// Does a HttpGET and parses the repsonse in to the response type T
        /// </summary>
        /// <typeparam name="T">Type of the result</typeparam>
        /// <param name="client">the HttpClient to use</param>
        /// <param name="url">the relative url to the endpoint to call</param>
        /// <returns>The API's response result of type T</returns>
        public static Task<T> GetAsync<T>(this HttpClient client, string url)
        {
            return client.GetAsync(url)
                .ContinueWith(t => t.Result.Content.ReadAsAsync<T>())
                .Unwrap();
        }

        /// <summary>
        /// Determines the API base url (from config), token information (from session) and does a HttpGET
        /// and parses the repsonse in to the response type T
        /// </summary>
        /// <typeparam name="T">Type of the result</typeparam>
        /// <param name="client">the HttpClient to use</param>
        /// <param name="url">the relative url to the endpoint to call</param>
        /// <returns>The API's response result of type T</returns>
        public static Task<T> SecureGetAsync<T>(this HttpClient client, string url)
        {
            return client
                .WithApiBaseUriFromConfig()
                .AddAuthFromSession(HttpContext.Current?.Session)
                .GetAsync<T>(url);
        }
        
        public async static Task<HttpClient> ConfigureFromConfig(this HttpClient client)
        {
            if (ConfigurationManager.AppSettings["apiUrl"] != null)
                client = client.WithApiBaseUriFromConfig();

            if (ConfigurationManager.AppSettings["AppUser"] != null && ConfigurationManager.AppSettings["AppPass"] != null)
                await client.Authenticate(ConfigurationManager.AppSettings["AppUser"].ToString(), ConfigurationManager.AppSettings["AppPass"].ToString());

            return client;
        }

        /// <summary>
        /// Generate and attach an encrypted signature to client as header value to be sent in all requests going forward
        /// </summary>
        /// <param name="client"></param>
        /// <param name="signature"></param>
        /// <param name="crypto"></param>
        /// <returns></returns>
        public static HttpClient SignRequestsWith(this HttpClient client, Signature signature, ICrypto<Signature> crypto)
        {
            try
            {
                var encryptedSignature = crypto.Encrypt(signature, ((MachineKeySection)ConfigurationManager.GetSection("system.web/machineKey")).DecryptionKey);
                return client.SignRequestsWith(encryptedSignature);
            }
            catch (Exception ex)
            {
                log.Error("Failed adding signature to HttpClient", ex);
            }

            return client;
        }

        public static HttpClient SignRequestsWith(this HttpClient client, string signature)
        {
            if(signature != null) client.DefaultRequestHeaders.Add("signature", signature);
            return client;
        }

        /// <summary>
        /// remove previous signature from the given http client to stop signing future requests
        /// </summary>
        /// <param name="client"></param>
        /// <returns></returns>
        public static HttpClient RemoveRequestSignature(this HttpClient client)
        {
            client.DefaultRequestHeaders.Remove("signature");
            return client;
        }
    }
}

I'm using owin and Ninject for my DI and when I construct a HttpClient instance I always do the same way (via DI) like this ...

var client = new HttpClient(new HttpClientHandler() { AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate })
                .WithApiBaseUriFromConfig()

... then ...
.AddAuthFromRequest(Request); // when in a HttpRequest Context
... or ...
.Authenticate(user, pass); // to add an auth header to all subsequent requests.

...
My thinking is that making HttpClient any smarter than "this is a REST service" or "this is an OData service" is probably going to result in chunks of business logic being held in the HttpClient.

Depending on your stance you may say that's a good thing but I have found that too much logic in the middle of complex stacks results in a key problem.

For example:

I have been building a multi tenancy based system that's a hybrid, CMS, DMS, other type "collection of systems" to serve our clients in a sort of middleware function, "think biztalk but as with a CMS" type thing.

I found that putting too many rules in my platform prevents users from using it in their specific scenarios.
And allowing my users to put logic in has to be done in a way that it doesn't affect the multi-tenancy "ness" of my platform.

So there's this balance that must be drawn.
That said ...

If I wanted to build something that's .Net based and talks to something like like VSTS or github ... it would be super cool to have a strongly typed API layer I can explore and having watched that video on youtube it's got me thinking of building a MyCompanyHttpClient.

Random question though ...
Why not use inheritance because naturally everything a HttpClient can do you can do to the Githhub service ... or is this a deliberate effort to "hide" the root level abstract functionality more general to HttpClient?

public class GitHubHttpClient : HttpClient { }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant