0

I've written a C# program that asynchronously makes an API call to a vendor to generate a submission token. This token is then attached to any form post payload to verify it's integrity. It works great as a stand alone .Net 4.6.1 application. The problem that I'm running into is integrating it into my CMS. After speaking with the CMS support, they asked me to make it a synchronous operation instead. I'm having trouble converting my code. Specifically the part about POSTing the form after I've retrieved the token.

Here is the source code that I originally wrote. Obviously AppID and secrets have been removed.

using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.IO;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using System.Web.UI;

namespace FormSubmission
{
    public partial class _Default : Page
    {
        protected async void Page_Load(object sender, EventArgs e)
        {
            var start = new Start();
            await start.KickOff();
        }

        public class Start
        {

            public async Task<String> KickOff()
            {
                var auth = new Authentication();
                var token = await auth.Authenticate();

                // return await Task.FromResult(token);
                // //Task.FromResult(token);
                if (!String.IsNullOrEmpty(token))
                {
                    var form = new FormSubmission();
                    var formFields = new Form();
                    formFields.createMethod = "SubmitForm";
                    formFields.email = "email@email.com";
                    formFields.mobile = "555-555-5555";
                    formFields.remark = "Hello, hope this works";
                    formFields.attr.attr3 = "mnc";
                    formFields.attr.attr6 = "1000";
                    formFields.attr.attr10 = "first name";
                    formFields.attr.attr11 = "lastname";
                    formFields.attr.attr14 = "City";
                    formFields.attr.attr15 = "State";
                    formFields.attr.attr18 = "USA";
                    formFields.attr.attr25 = "USA";
                    formFields.attr.attr28 = "Newsletter";

                    var serializer = JsonSerializer.Create();

                    var optionsString = new StringBuilder();
                    var writer = new StringWriter(optionsString);

                    serializer.Serialize(writer, formFields);

                    await form.Submit(token, optionsString.ToString());
                }

                return await Task.FromResult("");

            }
        }

        public class Authentication 
        {
            private const string appId = "XXXXXXXXXXXXX";
            private const string secret = "XXXXXXXXXXXXXXXXXXXXXX";

            public async Task<String> Authenticate()
            {
                string url = "https://api-url-goes-here";
                string token = "";

                try
                {

                    using (var client = new HttpClient())
                    {
                        var responseMessage = await client.GetAsync(url + appId + "&secret=" + secret);
                        var content = await responseMessage.Content.ReadAsStringAsync();
                        var contentObject = JObject.Parse(content);
                        token = contentObject["access_token"].ToString();
                        return token;
                    };
                }
                catch (Exception e)
                {
                    throw new Exception("Access token not found");
                }

            }
        }

        public class FormSubmission
        {

            public async Task Submit(string token, string json)
            {
                using (var client = new HttpClient())
                {
                    var message = await client.PostAsync(
                        "https://api-url-goes-here/access_token=" + token,
                        new StringContent(json, Encoding.UTF8, "application/json"));

                    var content = await message.Content.ReadAsStringAsync();
                    Console.Write(content);
                }
            }
        }
    }
}

This code works great when running in a new .Net project that matches my CMS's version of .Net. But again, they told me it doesn't support async operations (which seems weird but I'm not a great C# developer clearly)

So I've rewritten it.

public class Attr
{
    public string attr3;
    public string attr6;
    public string attr10;
    public string attr11;
    public string attr14;
    public string attr15;
    public string attr18;
    public string attr25;
    public string attr28;
}

public class FormSubmission
{
    public string mobile;
    public string email;
    public string remark;
    public string createMethod;
    public Attr attr = new Attr();
}
protected void Page_Load(object sender, EventArgs e)
{

    string token = fetchToken();


    var formFields = new FormSubmission();
    formFields.createMethod = "SubmitForm";
    formFields.email = "email@email.com";
    formFields.mobile = "5555555555";
    formFields.remark = "Non sync";
    formFields.attr.attr3 = "mnc";
    formFields.attr.attr6 = "1000";
    formFields.attr.attr10 = "first name";
    formFields.attr.attr11 = "last name";
    formFields.attr.attr14 = "City";
    formFields.attr.attr15 = "State";
    formFields.attr.attr18 = "USA";
    formFields.attr.attr25 = "USA";
    formFields.attr.attr28 = "Newsletter";

    var serializer = JsonSerializer.Create();

    var optionsString = new StringBuilder();
    var writer = new StringWriter(optionsString);

    serializer.Serialize(writer, formFields);


    var x = new submitForm();
    x.Submit(token, optionsString.ToString());
}

protected string fetchToken()
{

    string appId = "xxxxxxxxxxxxxxx";
    string secret = "xxxxxxxxxxxxxxxxxxxxxxxxxx";
    string url = "https://api-url-goes-here/security/accesstoken?grant_type=client_credentials&appid=";

    string token;

    try
    {
        HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url + appId + "&secret=" + secret);
        request.Method = "GET";
        request.KeepAlive = false;
        request.ContentType = "appication/json";

        HttpWebResponse response = (HttpWebResponse)request.GetResponse();

        string myResponse = "";

        using (System.IO.StreamReader sr = new System.IO.StreamReader(response.GetResponseStream()))
        {
            myResponse = sr.ReadToEnd();
        }

        JObject jobj = JObject.Parse(myResponse);
        token = jobj["access_token"].ToString();

        return token;


    }
    catch (Exception e)
    {
        return e.ToString();
    }

}

public class submitForm
{
    public void Submit(string token, string json)
    {
        using (var client = new HttpClient())
        {
            var message = client.PostAsync("https://api-url-goes-here/v1/customers?access_token=" + token,
                new StringContent(json, Encoding.UTF8, "application/json"));

        }
    }
}

As you can see, it's supposed to be the same code. The problem that I'm running to as at the Submit method on the submitForm class. When I run my program and have fiddler open, I can see the request made to get the token. And a token is received. It's the submitForm class that's giving me issues. I'm not even seeing a POST request in fiddler. Can anyone help me out?

Here is the class that is giving me problems.

public class submitForm
{
    public void Submit(string token, string json)
    {
        using (var client = new HttpClient())
        {
            var message = client.PostAsync("https://api-url-goes-here/v1/customers?access_token=" + token,
                new StringContent(json, Encoding.UTF8, "application/json"));

        }
    }
}
onTheInternet
  • 6,421
  • 10
  • 41
  • 74

2 Answers2

2
public void Submit(string token, string json)
{
    using (var client = new HttpClient())
    {
        var message = client.PostAsync("https://api-url-goes-here/v1/customers?access_token=" + token,
            new StringContent(json, Encoding.UTF8, "application/json"));

    }
}

This is not behaving as you would expect because message is now a Task, not the response you were hoping for since you are not waiting until the PostAsync completes.

To make this syncronous, you have two options:

  1. Call .GetAwaiter().GetResult() on it. That can be used anywhere you were using await before and no sync version of the method exists. For example:
public void Submit(string token, string json)
{
    using (var client = new HttpClient())
    {
        var message = client.PostAsync(
            "https://api-url-goes-here/access_token=" + token,
            new StringContent(json, Encoding.UTF8, "application/json")).GetAwaiter().GetResult();

        var content = message.Content.ReadAsStringAsync().GetAwaiter().GetResult();
        Console.Write(content);
    }
}

The reason for using .GetAwaiter().GetResult() instead of just .Result is described here (basically because of how exceptions get thrown).

Or,

  1. Make the same request with HttpWebRequest instead of HttpClient, like you did in the other part of your code, so you don't have to deal with the async-only methods.
Gabriel Luci
  • 38,328
  • 4
  • 55
  • 84
  • 1
    This is "sync over async", and is fundamentally dangerous and discouraged. Just... treat with suitable caution :) – Marc Gravell Dec 06 '18 at 16:15
  • @MarcGravell There are dangers that need to be avoided, yes, but once they're avoided it's no longer dangerous. But yes, I agree it's not ideal. I updated my answer to include the other option of just not using `HttpClient` at all. – Gabriel Luci Dec 06 '18 at 16:24
  • Using async method synchronously might work. But then if you are using external library you relying that it won't change it's internal implentation and cause your code to break. – Wanton Dec 06 '18 at 16:47
  • Thank you all for your comments. The above solution worked for me so I accepted the answer. However, I'm going to pursue the second option as it's clear from the conversation that the first one isn't ideal. I'll also submit my code to my cms's support team and see if they have any suggestions. – onTheInternet Dec 07 '18 at 13:26
  • 1
    There is also the [`WebClient`](https://learn.microsoft.com/en-us/dotnet/api/system.net.webclient) class in .NET. It's older than `HttpClient`, but has non-async methods and still makes things easier than `HttpWebRequest`. There's an example of how to send JSON with it [here](https://stackoverflow.com/a/25673701/1202807). And [here is](https://www.diogonunes.com/blog/webclient-vs-httpclient-vs-httpwebrequest/) an explanation of the differences between `HttpWebRequest`, `WebClient` and `HttpClient`. – Gabriel Luci Dec 07 '18 at 13:33
1

This is not probably what you want to hear but you should not convert async to synchronous method. You might get lucky that they work when you use GetAwaiter().GetResult() but there is a risk of deadlock or they just don't work properly. You could run them with Task.Run then GetAwaiter().GetResult() but that will spin another thread so it's not very efficient and your code is still blocking. Making it slower than synchronous code because it has to spin another thread. Plus it most surely allocates more memory that gc has to handle.

You should also name your async methods with Async suffix.

Wanton
  • 800
  • 6
  • 9