HttpWebRequest fire and forget

I need to do a post from a console app, It will be fire and forget. I do not need to wait for the response since the posting end point is middleware and it is doing all sorts of logging. The console app just need to fire the post with a data and its job is complete. Is my approach correct and that should i use #1 or #2?

I have a sample console app which is posting to free online testing service https://httpbin.org/post

I have a List that i am posting

Test#1: In this case i am just looping through List and calling CallProcess function. CallProcess function is is using Task.Run. I have commented the response code here since this kinds of end up not being truly async. To make it truly async, should i would need to use .GetResponseAsync and .ReadToEndAsync

foreach (var person in persons)
            {
                Console.WriteLine($"Processing {person.Id}");
                CallProcess(person);
            }

and then function

private static void CallProcess(Person person)
        {
            Console.WriteLine($"CallProcess called for {person.ToString()}");

            var endPoint = GetConfigValue("TargetEndpoint", "https://httpbin.org/post");

            Task.Run(() =>
            {
                try
                {
                    ServicePointManager.SecurityProtocol = (SecurityProtocolType)3072; // SecurityProtocolType.Tls12

                    HttpWebRequest request = (HttpWebRequest)WebRequest.Create(endPoint);
                    var data = Encoding.ASCII.GetBytes(FillMessage(person));
                    request.Method = GetConfigValue("TargetMethod", "POST");
                    request.ContentType = GetConfigValue("TargetContentType", applicationjson);
                    request.ContentLength = data.Length;
                    using (var stream = request.GetRequestStream())
                    {
                        stream.Write(data, 0, data.Length);
                    }
                    Console.WriteLine($"Push Complete for {person.Id}");
                    
                    
                    using(var response = (HttpWebResponse)request.GetResponse())
                    {
                        /*
                        var responseValue = "";
                        using (var reader = new StreamReader(response.GetResponseStream()))
                        {
                            responseValue = reader.ReadToEnd();
                        }
                        Console.WriteLine($"Person {person.Id} response : {responseValue}");  
                        */
                    }
                    
                }
                catch(Exception ex)
                {
                    Console.WriteLine($"ID {person.Id} has error: {ex.ToString()}");
                }
            });

        }

Test#2: In this case i have put Task.Run in the loop and then inside it i am calling the CallProcess2 function. Again, i have commented the response code.

foreach (var person in persons)
            {
                Task.Run(() =>
                {
                    try
                    {
                        Console.WriteLine($"Processing {person.Id}");
                        CallProcess2(person);
                    }
                    catch (Exception ex)
                    {
                        Console.WriteLine($"{ex.ToString()}");
                    }
                });
            }

and CallProcess2 function

private static void CallProcess2(Person person)
        {
            Console.WriteLine($"CallProcess called for {person.ToString()}");

            var endPoint = GetConfigValue("TargetEndpoint", "https://httpbin.org/post");

            ServicePointManager.SecurityProtocol = (SecurityProtocolType)3072; // SecurityProtocolType.Tls12

            HttpWebRequest request = (HttpWebRequest)WebRequest.Create(endPoint);
            var data = Encoding.ASCII.GetBytes(FillMessage(person));
            request.Method = GetConfigValue("TargetMethod", "POST");
            request.ContentType = GetConfigValue("TargetContentType", applicationjson);
            request.ContentLength = data.Length;
            using (var stream = request.GetRequestStream())
            {
                stream.Write(data, 0, data.Length);
            }
            Console.WriteLine($"Push Complete for {person.Id}");

            using(var response = (HttpWebResponse)request.GetResponse())
            {
                /*
                var responseValue = "";
                using (var reader = new StreamReader(response.GetResponseStream()))
                {
                    responseValue = reader.ReadToEnd();
                }
                Console.WriteLine($"Person {person.Id} response : {responseValue}");  
                */
            }            
        }

Here is the full code if you want to test it locally. .Net version is 4.5.1

Program.cs

using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Configuration;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Serialization;

//testing fire and forget
namespace WebRequestTest
{
    //using the free online testing service https://httpbin.org/post
    class Program
    {
        private const string applicationXml = "application/xml";
        private const string applicationjson = "application/json";

        static void Main(string[] args)
        {
            var persons = new List<Person>()
            {
                new Person(){ Id = 1, Name = "John Doe", Occupation = "Gardner"},
                new Person(){ Id = 2, Name = "John Smith", Occupation = "Tiles"},
                new Person(){ Id = 3, Name = "John Graham", Occupation = "Kitchen"}
            };

            Console.WriteLine("Data Order in List<T> Loop");
            Console.WriteLine("====================");
            foreach (var person in persons)
            {
                Console.WriteLine(person.ToString());
            }
            Console.WriteLine("********************");
            Console.WriteLine("");

            Console.WriteLine("Calling Process Loop");
            Console.WriteLine("====================");

            //Testing #1
            
            foreach (var person in persons)
            {
                Console.WriteLine($"Processing {person.Id}");
                CallProcess(person);
            }
            

            //Testing #2
            
            foreach (var person in persons)
            {
                Task.Run(() =>
                {
                    try
                    {
                        Console.WriteLine($"Processing {person.Id}");
                        CallProcess2(person);
                    }
                    catch (Exception ex)
                    {
                        Console.WriteLine($"{ex.ToString()}");
                    }
                });
            }
            
            
            Console.ReadKey();
        }

        private static void CallProcess(Person person)
        {
            Console.WriteLine($"CallProcess called for {person.ToString()}");

            var endPoint = GetConfigValue("TargetEndpoint", "https://httpbin.org/post");

            Task.Run(() =>
            {
                try
                {
                    ServicePointManager.SecurityProtocol = (SecurityProtocolType)3072; // SecurityProtocolType.Tls12

                    HttpWebRequest request = (HttpWebRequest)WebRequest.Create(endPoint);
                    var data = Encoding.ASCII.GetBytes(FillMessage(person));
                    request.Method = GetConfigValue("TargetMethod", "POST");
                    request.ContentType = GetConfigValue("TargetContentType", applicationjson);
                    request.ContentLength = data.Length;
                    using (var stream = request.GetRequestStream())
                    {
                        stream.Write(data, 0, data.Length);
                    }
                    Console.WriteLine($"Push Complete for {person.Id}");
                    
                    
                    using(var response = (HttpWebResponse)request.GetResponse())
                    {
                        /*
                        var responseValue = "";
                        using (var reader = new StreamReader(response.GetResponseStream()))
                        {
                            responseValue = reader.ReadToEnd();
                        }
                        Console.WriteLine($"Person {person.Id} response : {responseValue}");  
                        */
                    }
                    
                }
                catch(Exception ex)
                {
                    Console.WriteLine($"ID {person.Id} has error: {ex.ToString()}");
                }
            });

        }

        private static void CallProcess2(Person person)
        {
            Console.WriteLine($"CallProcess called for {person.ToString()}");

            var endPoint = GetConfigValue("TargetEndpoint", "https://httpbin.org/post");

            ServicePointManager.SecurityProtocol = (SecurityProtocolType)3072; // SecurityProtocolType.Tls12

            HttpWebRequest request = (HttpWebRequest)WebRequest.Create(endPoint);
            var data = Encoding.ASCII.GetBytes(FillMessage(person));
            request.Method = GetConfigValue("TargetMethod", "POST");
            request.ContentType = GetConfigValue("TargetContentType", applicationjson);
            request.ContentLength = data.Length;
            using (var stream = request.GetRequestStream())
            {
                stream.Write(data, 0, data.Length);
            }
            Console.WriteLine($"Push Complete for {person.Id}");

            using(var response = (HttpWebResponse)request.GetResponse())
            {
                /*
                var responseValue = "";
                using (var reader = new StreamReader(response.GetResponseStream()))
                {
                    responseValue = reader.ReadToEnd();
                }
                Console.WriteLine($"Person {person.Id} response : {responseValue}");  
                */
            }            
        }

        private static string FillMessage(Person person)
        {
            if (GetConfigValue("TargetContentType", applicationjson) == applicationXml)
            {
                var xmlData = ToXML(person);
                Console.WriteLine($"ID {person.Id} data: {xmlData}");
                return xmlData;
            }
            else if (GetConfigValue("TargetContentType", applicationjson) == applicationjson)
            {
                var jsonData = ToJSON(person);
                Console.WriteLine($"ID {person.Id} data: {jsonData}");
                return jsonData;
            }
            return string.Empty;
        }

        private static string ToXML(object person)
        {
            var xmlSerializer = new XmlSerializer(person.GetType());
            string result = string.Empty;
            using (StringWriter writer = new StringWriterBase())
            {
                xmlSerializer.Serialize(writer, person);
                result = writer.ToString();
            }
            return result;
        }

        private static string ToJSON(object person)
        {
            return JsonConvert.SerializeObject(person);
        }

        private static string GetConfigValue(string key, string defaultValue = null)
        {
            return ConfigurationManager.AppSettings[key] ?? defaultValue;
        }
    }

    public class Person
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Occupation { get; set; }

        public override string ToString()
        {
            return $"{Name} ({Id}): {Occupation}";
        }
    }

    public class StringWriterBase : StringWriter
    {
        public override Encoding Encoding => Encoding.UTF8;
    }
}

I do not see a question here therefore I assume this is an article explaining how to do something. So it would help to explain what it is that you are explaining. For example, what is the advantage of using Task.Run instead of GetResponseAsync and ReadToEndAsync?

Oh, sorry, I guess you do ask the question should i use #1 or #2? Well you can do it whatever way you want to, you do not need our permission.

Perhaps you actually want to ask a different question.

If you want opinion then my opinion is that that is not a good solution. You should at least check for errors.

I like option #2 better. My main issue with #1 is that CallProcess is writing something to the console when something went wrong. That gives CallProcess implied knowledge of where it’s being called from, so then it cannot be reused from a different context should you ever want to.

In fact I would fully get rid of the calls to Console in CallProcess2:

foreach (var person in persons)
{
    Task.Run(() =>
    {
        try
        {
            Console.WriteLine($"Processing {person.Id}");
            result = CallProcess2(person);
            Console.WriteLine($"Person {person.Id} response : {result}");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Error processing {person.Id}: {ex.ToString()}");
        }
    });
}

private static String CallProcess2(Person person)
{
    var endPoint = GetConfigValue("TargetEndpoint", "https://httpbin.org/post");

    ServicePointManager.SecurityProtocol = (SecurityProtocolType)3072; // SecurityProtocolType.Tls12

    HttpWebRequest request = (HttpWebRequest)WebRequest.Create(endPoint);
    var data = Encoding.ASCII.GetBytes(FillMessage(person));
    request.Method = GetConfigValue("TargetMethod", "POST");
    request.ContentType = GetConfigValue("TargetContentType", applicationjson);
    request.ContentLength = data.Length;
    using (var stream = request.GetRequestStream())
    {
        stream.Write(data, 0, data.Length);
    }
    
    using(var response = (HttpWebResponse)request.GetResponse())
    {
        var responseValue = "";
        using (var reader = new StreamReader(response.GetResponseStream()))
        {
            responseValue = reader.ReadToEnd();
        }
        return responseValue;  
    }            
}

Ideally I’d break out CallProcess (or at least the boring parts that set up the HTTP stuff) to it’s own class and interface for better testability, but that’s up to you to decide if that’s worth it in this case.

3 Likes

This!

And This!

I can’t support that enough through a like. Also, instead of using 3072, use the name of the SecurityProtocolType via its enumeration. Makes it a lot cleaner and won’t require a comment to understand it.

Be very careful here though on how you instantiate your requests. I’d have to check my own implementation of RestAPI class/interface, but IIRC, there is an underlying component that isn’t very thread-safe so making it a singleton and ensuring only one instance is instantiated might be necessary. I’ll try to remember to look, but if you run into random errors once you fully implement it, this is likely why!

2 Likes

Thanks for the replies guys. The posted code is a test bed hence using console.write. The final code will use a logger that will properly log the errors if any.

Here are two alternate approaches, what do you think about these?

#1

HttpWebRequest myRequest = (HttpWebRequest)WebRequest.Create(myUrl);
//set up web request...
ThreadPool.QueueUserWorkItem(o=>{ myRequest.GetResponse(); });

#2

FireAndForget().Wait();

private async Task FireAndForget()
    {
        using (var httpClient = new HttpClient())
        {
            HttpCookie cookie = this.Request.Cookies["AuthCookieName"];
            var authToken = cookie["AuthTokenPropertyName"] as string;
            httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", authToken);
            using (var response = await httpClient.GetAsync("http://localhost/api/FireAndForgetApiMethodName"))
            {
                //will throw an exception if not successful
                response.EnsureSuccessStatusCode();
            }
        }
    }

Basically my final object is to create generic methods for Get, Put, Post and Delete, something along these lines

Class to pass back the response from CRUD operations

using System.Net;

namespace Test.Helpers
{
    public class ApiCallResult where X : class
    {
        public HttpStatusCode StatusCode { get; set; }
        public string ReasonPhrase { get; set; }

        public bool IsError { get; set; }
        public bool IsException { get; set; }
        public string Message { get; set; }

        public X ResponseObject { get; set; } //deserialized object, could be List, int string or just a single object
    }
}

and then the helper class

using System;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;

namespace Test.Helpers
{
    public static class ApiCrudCallHelper
    {
        /// <summary>
        /// Performs Post and returns ApiCallResult
        /// </summary>
        /// <typeparam name="T">model to Post, could be null, T, List T</typeparam>
        /// <typeparam name="X">return model by API, could be X, List X, string </typeparam>
        /// <param name="data">data to post of type T, List T</param>
        /// <param name="apiUrl">api full URL like http://localhost:65152/API/Test if executing custom action, provide that as well at the end </param>
        /// <returns>
        /// ApiCallResult
        ///     StatusCode: status code returned by the API
        ///     ReasonPhrase: reason phrase returned by the API
        ///     IsError: true/false
        ///     IsException: true/false
        ///     Message: error message, exception message, or result of OK etc results by API
        ///     X ResponseObject: model returned by the API, it might not be available in all cases. Could be X, List X or string as provided by X above
        /// </returns>
        public static async Task<ApiCallResult<X>> Post<T, X>(T data, string apiUrl) where X : class
        {
            var apiCallResult = new ApiCallResult<X> { IsError = true, Message = "No run" };
            try
            {
                //json string 
                var jsonString = JsonConvert.SerializeObject(data);
                using (var client = new HttpClient())
                {
                    var httpContent = new StringContent(jsonString, Encoding.UTF8, "application/json");
                    var response = await client.PostAsync(apiUrl, httpContent);
                    var jsonResponseString = await response.Content.ReadAsStringAsync();

                    //fill
                    if (response.IsSuccessStatusCode)
                    {
                        //deserialize
                        if (!typeof(X).Equals(typeof(string)))
                        {
                            apiCallResult.ResponseObject = JsonConvert.DeserializeObject<X>(jsonResponseString);
                        }
                        apiCallResult.IsError = false;
                    }
                    apiCallResult.StatusCode = response.StatusCode;
                    apiCallResult.ReasonPhrase = response.ReasonPhrase;
                    apiCallResult.Message = jsonResponseString;
                }
            }
            catch (Exception ex)
            {
                apiCallResult.IsException = true;
                apiCallResult.Message = ex.Message;
            }

            return apiCallResult;
        }

        /// <summary>
        /// Performs Put and returns ApiCallResult
        /// </summary>
        /// <typeparam name="T">model to Post, could be null, T, List T</typeparam>
        /// <typeparam name="X">return model by API, could be X, List X, string </typeparam>
        /// <param name="data">data to post of type T, List T</param>
        /// <param name="apiUrl">api full URL including the Id like http://localhost:65152/API/Test/12345 if executing custom action, provide that as well </param>
        /// <returns>
        /// ApiCallResult
        ///     HttpStatusCode StatusCode: status code returned by the API
        ///     string ReasonPhrase: reason phrase returned by the API
        ///     bool IsError: true/false
        ///     bool IsException: true/false
        ///     string Message: error message, exception message, or result of OK etc results by API
        ///     X ResponseObject: model returned by the API, it might not be available in all cases. Could be X, List X or string as provided by X above
        /// </returns>
        public static async Task<ApiCallResult<X>> Put<T, X>(T data, string apiUrl) where X : class
        {
            var apiCallResult = new ApiCallResult<X> { IsError = true, Message = "No run" };
            try
            {
                //json string 
                var jsonString = JsonConvert.SerializeObject(data);
                using (var client = new HttpClient())
                {
                    var httpContent = new StringContent(jsonString, Encoding.UTF8, "application/json");
                    var response = await client.PutAsync(apiUrl, httpContent);
                    var jsonResponseString = await response.Content.ReadAsStringAsync();

                    //fill
                    if (response.IsSuccessStatusCode)
                    {
                        //deserialize
                        if (!typeof(X).Equals(typeof(string)))
                        {
                            apiCallResult.ResponseObject = JsonConvert.DeserializeObject<X>(jsonResponseString);
                        }
                        apiCallResult.IsError = false;
                    }
                    apiCallResult.StatusCode = response.StatusCode;
                    apiCallResult.ReasonPhrase = response.ReasonPhrase;
                    apiCallResult.Message = jsonResponseString;
                }
            }
            catch (Exception ex)
            {
                apiCallResult.IsException = true;
                apiCallResult.Message = ex.Message;
            }

            return apiCallResult;
        }

        /// <summary>
        /// Performs Delete and returns ApiCallResult
        /// </summary>
        /// <typeparam name="X">return model by API, could be X, List X, string. Usually you'll only get Ok result etc for delete, so specify string  </typeparam>
        /// <param name="apiUrl">api full URL including the Id like http://localhost:65152/API/Test/12345 if executing custom action, provide that as well </param>
        /// <returns>
        /// ApiCallResult
        ///     HttpStatusCode StatusCode: status code returned by the API
        ///     string ReasonPhrase: reason phrase returned by the API
        ///     bool IsError: true/false
        ///     bool IsException: true/false
        ///     string Message: error message, exception message, or result of OK etc results by API
        ///     X ResponseObject: will only be available if api is returning a model (should not), in most cases it will not be available. Could be X, List X or string as provided by X above
        /// </returns>
        public static async Task<ApiCallResult<X>> Delete<X>(string apiUrl) where X : class
        {
            var apiCallResult = new ApiCallResult<X> { IsError = true, Message = "No run" };
            try
            {
                using (var client = new HttpClient())
                {
                    var response = await client.DeleteAsync(apiUrl);
                    var jsonResponseString = await response.Content.ReadAsStringAsync();

                    //fill
                    if (response.IsSuccessStatusCode)
                    {
                        //deserialize
                        if (!typeof(X).Equals(typeof(string)))
                        {
                            apiCallResult.ResponseObject = JsonConvert.DeserializeObject<X>(jsonResponseString);
                        }
                        apiCallResult.IsError = false;
                    }
                    apiCallResult.StatusCode = response.StatusCode;
                    apiCallResult.ReasonPhrase = response.ReasonPhrase;
                    apiCallResult.Message = jsonResponseString;
                }
            }
            catch (Exception ex)
            {
                apiCallResult.IsException = true;
                apiCallResult.Message = ex.Message;
            }

            return apiCallResult;
        }

        /// <summary>
        /// Performs Get and returns ApiCallResult
        /// </summary>
        /// <typeparam name="X">return model by API, could be X, List X, string. </typeparam>
        /// <param name="apiUrl">api full URL </param>
        /// <returns>
        /// ApiCallResult
        ///     HttpStatusCode StatusCode: status code returned by the API
        ///     string ReasonPhrase: reason phrase returned by the API
        ///     bool IsError: true/false
        ///     bool IsException: true/false
        ///     string Message: error message, exception message, or result of OK etc results by API
        ///     X ResponseObject: Could be X, List X or string as provided by X above
        /// </returns>
        public static async Task<ApiCallResult<X>> Get<X>(string apiUrl) where X : class
        {
            var apiCallResult = new ApiCallResult<X> { IsError = true, Message = "No run" };
            try
            {
                using (var client = new HttpClient())
                {
                    var response = await client.GetAsync(apiUrl);
                    var jsonResponseString = await response.Content.ReadAsStringAsync();

                    //fill
                    if (response.IsSuccessStatusCode)
                    {
                        //deserialize
                        if (!typeof(X).Equals(typeof(string)))
                        {
                            apiCallResult.ResponseObject = JsonConvert.DeserializeObject<X>(jsonResponseString);
                        }
                        apiCallResult.IsError = false;
                    }
                    apiCallResult.StatusCode = response.StatusCode;
                    apiCallResult.ReasonPhrase = response.ReasonPhrase;
                    apiCallResult.Message = jsonResponseString;
                }
            }
            catch (Exception ex)
            {
                apiCallResult.IsException = true;
                apiCallResult.Message = ex.Message;
            }

            return apiCallResult;
        }
    }
}

I have made some adjustments. What do you think about this? All errors will be logged later, currently being returned to the caller.

I have not tested it yet but it compiles.

To call the methods, i’ll use one of the following two

#1:

Task t = new Task(ClassName.MethodName);
 t.Start();

#2:
ClassName.MethodNamet().Wait();

Approach

  • The caller needs to fill the “basic info” and pass to the methods. Class is “ApiCrudCallBasics”. It has some helper methods and two validation routines as well. Some of the stuff will get cleaned later. Url, ContentType, Encoding, Authorization etc will be filled in here.

  • The caller needs to pass the data to post/put methods

  • The caller also neeeds to specify the expected return type of the data

ApiCrudCallBasics Class

using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Text;
using System.Xml.Serialization;

namespace WebRequestTest
{
    public class ApiCrudCallBasics
    {
        #region Constants

        public const string ContentTypeApplicationJson = "application/json";
        public const string ContentTypeApplicationXml = "application/xml";

        public const int Encoding_Id_Windows_1252 = 1252;
        public const string Encoding_Id_Iso_8859_1 = "ISO-8859-1";

        public const string HeaderAuthorizationTypeBasic = "Basic";
        public const string HeaderAuthorizationTypeBearer = "Bearer";

        #endregion

        #region Object Properties 

        public string RequestContentType { get; set; } = ContentTypeApplicationJson;

        /// <summary>
        /// Default is Encoding.UTF8
        /// </summary>
        public Encoding RequestEncoding { get; set; } = Encoding.UTF8;

        /// <summary>
        /// Full url with controller and action and any query string params
        /// </summary>
        public string EndPointFullUrl { get; set; }

        /// <summary>
        /// Default is Basic
        /// </summary>
        public string AuthorizationType { get; set; } = HeaderAuthorizationTypeBasic;

        /// <summary>
        /// Default is true, the AuthorizationValue will be converted to Base64String
        /// </summary>
        public bool AuthorizationValueConvertToBase64 { get; set; } = true;

        /// <summary>
        /// Provide UserName:Password in case of Basic, Also set the AuthorizationValueConvertToBase64 boolean to true/false 
        /// Provide token incase of Bearer, Also set the AuthorizationValueConvertToBase64 boolean to true/false
        /// </summary>
        public string AuthorizationValue { get; set; }

        /// <summary>
        /// build string, AuthorizationValue will be converted to Base64String in case AuthorizationValueConvertToBase64=True
        /// </summary>
        public string AuthorizationValueString
        {
            get
            {
                var aValue = AuthorizationValue;
                if (!string.IsNullOrWhiteSpace(aValue) && AuthorizationValueConvertToBase64)
                {
                    aValue = Convert.ToBase64String(Encoding.UTF8.GetBytes(AuthorizationValue));
                }
                return aValue;
            }
        }

        /// <summary>
        /// Need to set for the test api since the api is https
        /// Pass as SecurityProtocolType.Tls12; //(SecurityProtocolType)3072;
        /// Pass null if not https or make the changes accordingly
        /// </summary>
        public SecurityProtocolType? SecurityProtocolType {get; set;}

        #endregion

        #region Static Methods 

        public static byte[] ConvertEncoding(Encoding sourceEncoding, Encoding destinationEncoding, byte[] dataBytes)
        {
            var converted = Encoding.Convert(sourceEncoding, destinationEncoding, dataBytes);
            return converted;
        }

        public static byte[] ConvertEncoding(Encoding sourceEncoding, Encoding destinationEncoding, string data)
        {
            var sourceDataBytes = sourceEncoding.GetBytes(data);
            var converted = Encoding.Convert(sourceEncoding, destinationEncoding, sourceDataBytes);
            return converted;
        }

        public static Encoding GetEncoding(int forEncoding)
        {
            return Encoding.GetEncoding(forEncoding);
        }

        public static Encoding GetEncoding(string forEncoding)
        {
            return Encoding.GetEncoding(forEncoding);
        }

        /// <summary>
        /// Serializes the data per the content type provided
        /// </summary>
        /// <typeparam name="T">model to Post, could be null, T, List T</typeparam>
        /// <param name="data">data to post of type T, List T</param>
        /// <param name="requestContentType">ApiCrudCallBasics.ContentTypeApplicationJson (application/json) Or ApiCrudCallBasics.ContentTypeApplicationXml (application/xml)</param>
        /// <returns></returns>
        public static string SerializeData<T>(T data, string requestContentType)
        {
            var serializedData = string.Empty;

            if (data == null)
                return serializedData;

            if (requestContentType == ApiCrudCallBasics.ContentTypeApplicationJson)
            {
                serializedData = JsonConvert.SerializeObject(data);
            }
            else if (requestContentType == ApiCrudCallBasics.ContentTypeApplicationXml)
            {
                var xmlSerializer = new XmlSerializer(typeof(T));
                using (StringWriter writer = new StringWriterBase())
                {
                    xmlSerializer.Serialize(writer, data);
                    serializedData = writer.ToString();
                }
            }
            return serializedData;
        }

        #endregion

        #region Validator

        public static string ValidateBasicInfoMin(ApiCrudCallBasics basicInfo)
        {

            if (basicInfo == null)
            {
                return "Basic information object is null";
            }

            var errors = new List<string>();

            if (string.IsNullOrWhiteSpace(basicInfo.EndPointFullUrl))
                errors.Add($"{nameof(basicInfo.EndPointFullUrl)} is required");

            if (!string.IsNullOrWhiteSpace(basicInfo.AuthorizationValue) && string.IsNullOrWhiteSpace(basicInfo.AuthorizationType))
                errors.Add($"{nameof(basicInfo.AuthorizationType)} cannot be empty when providing {nameof(basicInfo.AuthorizationValue)}");

            var message = PutValidationErrorsTogether(errors);
            return message;
        }

        public static string ValidateBasicInfo(ApiCrudCallBasics basicInfo)
        {
            //min check
            var message = ValidateBasicInfoMin(basicInfo);
            if (!string.IsNullOrWhiteSpace(message))
            {
                return message;
            }

            //do the rest of the checks             
            var errors = new List<string>();

            if (string.IsNullOrWhiteSpace(basicInfo.RequestContentType))
                errors.Add($"{nameof(basicInfo.RequestContentType)} is required");

            if (basicInfo.RequestEncoding == null)
                errors.Add($"{nameof(basicInfo.RequestEncoding)} is required");

            if (errors.Count <= 0 )
                return string.Empty;

            message = PutValidationErrorsTogether(errors);
            return message;
        }

        private static string PutValidationErrorsTogether(List<string> errors)
        {
            if (errors == null || errors.Count <= 0)
                return string.Empty;

            var message = $"Basic info error {Environment.NewLine}{string.Join(Environment.NewLine, errors)}";
            return message;
        }
        

        #endregion
    }
}

ApiCrudCallResult
This is the result returned by the methods. It hasn’t changed since what i have posted above

using System.Net;

namespace WebRequestTest
{
    public class ApiCrudCallResult<X> where X : class
    {
        public HttpStatusCode StatusCode { get; set; }
        public string ReasonPhrase { get; set; }

        public bool IsError { get; set; }
        public bool IsException { get; set; }
        public string Message { get; set; }

        /// <summary>
        /// deserialized object, could be List, int string or just a single object
        /// </summary>
        public X ResponseObject { get; set; } 
    }
}

ApiCrudCallHelper

using Newtonsoft.Json;
using System;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;

namespace WebRequestTest
{
    public static class ApiCrudCallHelper
    {
        /// <summary>
        /// Performs Post and returns ApiCallResult
        /// </summary>
        /// <typeparam name="T">model to Post, could be null, T, List T</typeparam>
        /// <typeparam name="X">return model by API, could be X, List X, string </typeparam>
        /// <param name="data">data to post of type T, List T</param>
        /// <param name="basicInfo">the basic info to successfully make a call</param>
        /// <returns>
        /// ApiCallResult
        ///     StatusCode: status code returned by the API
        ///     ReasonPhrase: reason phrase returned by the API
        ///     IsError: true/false
        ///     IsException: true/false
        ///     Message: error message, exception message, or result of OK etc results by API
        ///     X ResponseObject: model returned by the API, it might not be available in all cases. Could be X, List X or string as provided by X above
        /// </returns>
        public static async Task<ApiCrudCallResult<X>> Post<T, X>(T data, ApiCrudCallBasics basicInfo) where X : class
        {
            var apiCallResult = new ApiCrudCallResult<X> { IsError = true, Message = "No run" };

            try
            {
                //check basic info
                var error = ApiCrudCallBasics.ValidateBasicInfo(basicInfo);
                if (!string.IsNullOrWhiteSpace(error))
                {
                    apiCallResult.Message = error;
                    return apiCallResult;
                }

                //check if security protocol provided
                if(basicInfo.SecurityProtocolType != null)
                {
                    ServicePointManager.SecurityProtocol = basicInfo.SecurityProtocolType.GetValueOrDefault(); //(SecurityProtocolType)3072; // SecurityProtocolType.Tls12
                }

                //data string 
                var dataString = ApiCrudCallBasics.SerializeData<T>(data, basicInfo.RequestContentType);
                using (var client = new HttpClient())
                {
                    //basic authorization
                    if (!string.IsNullOrWhiteSpace(basicInfo.AuthorizationValue))
                    {
                        client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue(basicInfo.AuthorizationType, basicInfo.AuthorizationValueString);
                    }

                    var httpContent = new StringContent(dataString, basicInfo.RequestEncoding, basicInfo.RequestContentType);
                    var response = await client.PostAsync(basicInfo.EndPointFullUrl, httpContent);
                    var jsonResponseString = await response.Content.ReadAsStringAsync();

                    //fill
                    if (response.IsSuccessStatusCode)
                    {
                        //deserialize
                        if (!typeof(X).Equals(typeof(string)))
                        {
                            apiCallResult.ResponseObject = JsonConvert.DeserializeObject<X>(jsonResponseString);
                        }
                        apiCallResult.IsError = false;
                    }
                    apiCallResult.StatusCode = response.StatusCode;
                    apiCallResult.ReasonPhrase = response.ReasonPhrase;
                    apiCallResult.Message = jsonResponseString;
                }
            }
            catch (Exception ex)
            {
                apiCallResult.IsException = true;
                apiCallResult.Message = ex.Message;
            }

            return apiCallResult;
        }

        /// <summary>
        /// Performs Put and returns ApiCallResult
        /// </summary>
        /// <typeparam name="T">model to Post, could be null, T, List T</typeparam>
        /// <typeparam name="X">return model by API, could be X, List X, string </typeparam>
        /// <param name="data">data to post of type T, List T</param>
        /// <param name="basicInfo">the basic info to successfully make a call</param>
        /// <returns>
        /// ApiCallResult
        ///     HttpStatusCode StatusCode: status code returned by the API
        ///     string ReasonPhrase: reason phrase returned by the API
        ///     bool IsError: true/false
        ///     bool IsException: true/false
        ///     string Message: error message, exception message, or result of OK etc results by API
        ///     X ResponseObject: model returned by the API, it might not be available in all cases. Could be X, List X or string as provided by X above
        /// </returns>
        public static async Task<ApiCrudCallResult<X>> Put<T, X>(T data, ApiCrudCallBasics basicInfo) where X : class
        {
            var apiCallResult = new ApiCrudCallResult<X> { IsError = true, Message = "No run" };
            try
            {
                //check basic info
                var error = ApiCrudCallBasics.ValidateBasicInfo(basicInfo);
                if (!string.IsNullOrWhiteSpace(error))
                {
                    apiCallResult.Message = error;
                    return apiCallResult;
                }

                //check if security protocol provided
                if (basicInfo.SecurityProtocolType != null)
                {
                    ServicePointManager.SecurityProtocol = basicInfo.SecurityProtocolType.GetValueOrDefault(); //(SecurityProtocolType)3072; // SecurityProtocolType.Tls12
                }

                //data string 
                var dataString = ApiCrudCallBasics.SerializeData<T>(data, basicInfo.RequestContentType);
                using (var client = new HttpClient())
                {
                    //basic authorization
                    if (!string.IsNullOrWhiteSpace(basicInfo.AuthorizationValue))
                    {
                        client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue(basicInfo.AuthorizationType, basicInfo.AuthorizationValueString);
                    }

                    var httpContent = new StringContent(dataString, basicInfo.RequestEncoding, basicInfo.RequestContentType);
                    var response = await client.PutAsync(basicInfo.EndPointFullUrl, httpContent);
                    var jsonResponseString = await response.Content.ReadAsStringAsync();

                    //fill
                    if (response.IsSuccessStatusCode)
                    {
                        //deserialize
                        if (!typeof(X).Equals(typeof(string)))
                        {
                            apiCallResult.ResponseObject = JsonConvert.DeserializeObject<X>(jsonResponseString);
                        }
                        apiCallResult.IsError = false;
                    }
                    apiCallResult.StatusCode = response.StatusCode;
                    apiCallResult.ReasonPhrase = response.ReasonPhrase;
                    apiCallResult.Message = jsonResponseString;
                }
            }
            catch (Exception ex)
            {
                apiCallResult.IsException = true;
                apiCallResult.Message = ex.Message;
            }

            return apiCallResult;
        }

        /// <summary>
        /// Performs Delete and returns ApiCallResult
        /// </summary>
        /// <typeparam name="X">return model by API, could be X, List X, string. Usually you'll only get Ok result etc for delete, so specify string  </typeparam>
        /// <param name="basicInfo">the basic info to successfully make a call</param>
        /// <returns>
        /// ApiCallResult
        ///     HttpStatusCode StatusCode: status code returned by the API
        ///     string ReasonPhrase: reason phrase returned by the API
        ///     bool IsError: true/false
        ///     bool IsException: true/false
        ///     string Message: error message, exception message, or result of OK etc results by API
        ///     X ResponseObject: will only be available if api is returning a model (should not), in most cases it will not be available. Could be X, List X or string as provided by X above
        /// </returns>
        public static async Task<ApiCrudCallResult<X>> Delete<X>(ApiCrudCallBasics basicInfo) where X : class
        {
            var apiCallResult = new ApiCrudCallResult<X> { IsError = true, Message = "No run" };
            try
            {
                //check basic info
                var error = ApiCrudCallBasics.ValidateBasicInfoMin(basicInfo);
                if (!string.IsNullOrWhiteSpace(error))
                {
                    apiCallResult.Message = error;
                    return apiCallResult;
                }

                //check if security protocol provided
                if (basicInfo.SecurityProtocolType != null)
                {
                    ServicePointManager.SecurityProtocol = basicInfo.SecurityProtocolType.GetValueOrDefault(); //(SecurityProtocolType)3072; // SecurityProtocolType.Tls12
                }

                using (var client = new HttpClient())
                {
                    //basic authorization
                    if (!string.IsNullOrWhiteSpace(basicInfo.AuthorizationValue))
                    {
                        client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue(basicInfo.AuthorizationType, basicInfo.AuthorizationValueString);
                    }

                    var response = await client.DeleteAsync(basicInfo.EndPointFullUrl);
                    var jsonResponseString = await response.Content.ReadAsStringAsync();

                    //fill
                    if (response.IsSuccessStatusCode)
                    {
                        //deserialize
                        if (!typeof(X).Equals(typeof(string)))
                        {
                            apiCallResult.ResponseObject = JsonConvert.DeserializeObject<X>(jsonResponseString);
                        }
                        apiCallResult.IsError = false;
                    }
                    apiCallResult.StatusCode = response.StatusCode;
                    apiCallResult.ReasonPhrase = response.ReasonPhrase;
                    apiCallResult.Message = jsonResponseString;
                }
            }
            catch (Exception ex)
            {
                apiCallResult.IsException = true;
                apiCallResult.Message = ex.Message;
            }

            return apiCallResult;
        }

        /// <summary>
        /// Performs Get and returns ApiCallResult
        /// </summary>
        /// <typeparam name="X">return model by API, could be X, List X, string. </typeparam>
        /// <param name="basicInfo">the basic info to successfully make a call</param>
        /// <returns>
        /// ApiCallResult
        ///     HttpStatusCode StatusCode: status code returned by the API
        ///     string ReasonPhrase: reason phrase returned by the API
        ///     bool IsError: true/false
        ///     bool IsException: true/false
        ///     string Message: error message, exception message, or result of OK etc results by API
        ///     X ResponseObject: Could be X, List X or string as provided by X above
        /// </returns>
        public static async Task<ApiCrudCallResult<X>> Get<X>(ApiCrudCallBasics basicInfo) where X : class
        {
            var apiCallResult = new ApiCrudCallResult<X> { IsError = true, Message = "No run" };
            try
            {
                //check basic info
                var error = ApiCrudCallBasics.ValidateBasicInfo(basicInfo);
                if (!string.IsNullOrWhiteSpace(error))
                {
                    apiCallResult.Message = error;
                    return apiCallResult;
                }

                //check if security protocol provided
                if (basicInfo.SecurityProtocolType != null)
                {
                    ServicePointManager.SecurityProtocol = basicInfo.SecurityProtocolType.GetValueOrDefault(); //(SecurityProtocolType)3072; // SecurityProtocolType.Tls12
                }

                using (var client = new HttpClient())
                {
                    var response = await client.GetAsync(basicInfo.EndPointFullUrl);
                    var jsonResponseString = await response.Content.ReadAsStringAsync();

                    //fill
                    if (response.IsSuccessStatusCode)
                    {
                        //deserialize
                        if (!typeof(X).Equals(typeof(string)))
                        {
                            apiCallResult.ResponseObject = JsonConvert.DeserializeObject<X>(jsonResponseString);
                        }
                        apiCallResult.IsError = false;
                    }
                    apiCallResult.StatusCode = response.StatusCode;
                    apiCallResult.ReasonPhrase = response.ReasonPhrase;
                    apiCallResult.Message = jsonResponseString;
                }
            }
            catch (Exception ex)
            {
                apiCallResult.IsException = true;
                apiCallResult.Message = ex.Message;
            }

            return apiCallResult;
        }
    }
}

A few suggestions:

  • Rename ApiCrudCallBasics to Request
  • Rename ApiCrudCallResult to Response

Those two alone will in one single sweep make the code a lot more readable for outsiders. ApiCrudCallBasics and ApiCrudCallResult are something none of us have ever seen, but all of us know request - response.

  • Instead of creating a Request and then letting it validate itself (using ValidateBasicInfoMin), give Request a constructor and prevent it to be constructed from incorrect values. That way you can never forget to call the ValidateBasicInfoMin method and thus have a Request in an incorrect state.

Thanks for the suggestions, i have made the changes in test bed.

I have also created overload POST/PUT/Delete methods. I have done the renaming but posting here with original name since above posted code is using the same name.

public static async Task<ApiCrudCallResult<X>> Post<T, X>(T data, ApiCrudCallHelper basicInfo) where X : class 

public static async Task Post<T>(T data, ApiCrudCallHelper basicInfo) 

public static async Task<ApiCrudCallResult<X>> Put<T, X>(T data, ApiCrudCallHelper basicInfo) where X : class

public static async Task Put<T>(T data, ApiCrudCallHelper basicInfo) 

public static async Task<ApiCrudCallResult<X>> Delete<X>(ApiCrudCallHelper basicInfo) where X : class 

public static async Task Delete(ApiCrudCallHelper basicInfo)

public static async Task<ApiCrudCallResult<X>> Get<X>(ApiCrudCallHelper basicInfo) where X : class

The intention is to call these in following two ways

#1
var postResult = await ApiCrudCallUtility.Post<Person, PersonResponse>(person, basicInfo);

or

#2
ApiCrudCallUtility.Post<Person>(person, basicInfo).Wait();

So the methods that do not return the response do the post as

var httpContent = new StringContent(dataString, basicInfo.RequestEncoding, basicInfo.RequestContentType);

                    using (var response = await client.PostAsync(basicInfo.EndPointFullUrl, httpContent))
                    {
                        //will throw an exception if not successful
                        response.EnsureSuccessStatusCode();
                    }

This topic was automatically closed 91 days after the last reply. New replies are no longer allowed.