2

I'm not very familiar with asynchronized calls but I ran into a bit of what I think is a weird issue and not sure why it's happening.

CONCEPT Call a function to retrieve data. If data is stored in our cache, we use Redis as our cache (in case it matters on how the data should be stored as I've read on other threads), then return it. Otherwise, make a call to a third party library (specifically Force.com Toolkit for .Net, but I doubt it matters) which uses async calls and cache the results.

I created a synchronous method which works, but now I want to change it to be asynchronous.

Synchronous

public static Models.Description.ObjectDescription Describe(ForceClient forceClient, string sObject) 
{
    Models.Description.ObjectDescription result;
    var cacheName = "Salesforce_Object_" + sObject;
    if (HttpContext.Current.Cache[cacheName] != null) 
    {
        result = (Models.Description.ObjectDescription) HttpContext.Current.Cache[cacheName];
    } 
    else 
    {
        result = forceClient.DescribeAsync<Models.Description.ObjectDescription>(sObject).Result;

        if (result != null) 
        {
            var expiration = 30; // testing, this will be read from a global variable
            HttpContext.Current.Cache.Insert(
                cacheName, 
                result, 
                null, 
                DateTime.UtcNow.AddSeconds(expiration), 
                Cache.NoSlidingExpiration, 
                CacheItemPriority.Default, 
                null);
        }
    }
    return result;
}

Asynchronous full code

public static class Description {
    public static async Task<Models.Description.ObjectDescription> Describe(ForceClient forceClient, string sObject) 
    {
        Models.Description.ObjectDescription result;
        var cacheName = "Salesforce_Object_" + sObject;
        if (HttpContext.Current.Cache[cacheName] != null) 
        {
            result = (Models.Description.ObjectDescription) HttpContext.Current.Cache[cacheName];
        } 
        else 
        {
            /*
            * only line that changed from above
            */
            result = await forceClient.DescribeAsync<Models.Description.ObjectDescription>(sObject);

            if (result != null) 
            {
                var expiration = 30; // testing, this will be read from a global variable
                HttpContext.Current.Cache.Insert(
                    cacheName, 
                    result, 
                    null, 
                    DateTime.UtcNow.AddSeconds(expiration), 
                    Cache.NoSlidingExpiration, 
                    CacheItemPriority.Default, 
                    null);
            }
        }
        return result;
    }

    public static async Task<IList<Models.Description.PicklistValues>> Picklist(ForceClient forceClient, string sObject, string fieldName) {
        var results = await Describe(forceClient, sObject);

        var field = results.Fields.SingleOrDefault(f => f.Name.Equals(fieldName));

        return field != null ? field.PickListValues : new List<Models.Description.PicklistValues>();
    }
}       
public static class Lead {
    public static async Task<IList<Models.Description.PicklistValues>> Picklist(ForceClient forceClient, string fieldName) {
        return await Description.Picklist(forceClient, "Lead", fieldName);
    }
}

Page

protected void Page_Load(object sender, EventArgs e) {
    /* when I try to `await` this i get an error stating:
    *  The 'await' operator can only be used with an async method.
    *  ...but when I change Page_Load to
    *  protected async Task Page_Load(object sender, EventArgs e) {...}
    *  ... and 'await' the call, My results are blank
    */
    var result = Lead.Picklist(new Authentication.ForceClient(), "Source__c").Result;

    foreach (var value in result) {
        Response.Write("- " + value.Value + "<br />");
    }
}

The Synchronous version works great, but when I convert it to Asynchronous I get an error on the line HttpContext.Current.Cache.Insert(...)

Exception Details: System.NullReferenceException: Object reference not set to an instance of an object.

Currently, this site is a WebForms website so to debug I use Response.Write(). When I try to display the cacheName variable, I get the same results.

Exception Details: System.NullReferenceException: Object reference not set to an instance of an object.

Line 30: HttpContext.Current.Response.Write(cacheName + "<br>");

Is the variable cacheName being lost when I make the async call? I highly doubt it, but I'm not sure how to proceed.

Hopefully someone can help.

RoLYroLLs
  • 3,113
  • 4
  • 38
  • 57
  • A `NullReferenceException` means something you're trying to access is `null`. When you debug, what is `null`? – David Aug 16 '17 at 15:34
  • @David that's what I'm trying to find out. Did you not read my question entirely? I do set `cacheName` and later whenI try to access it, I get the error. As for debugging, I just showed you my way of debugging `WebForms`. Is there another way? – RoLYroLLs Aug 16 '17 at 15:38
  • 3
    After an async call Thread Static variables may be lost. So, save `HttpContext.Current` to a variable before calling async method and then use it. – L.B Aug 16 '17 at 15:38
  • @RoLYroLLs: Yes, you can use a debugger. Did you read the linked duplicate? Unless you're dereferencing the `cacheName` object somewhere, that's unlikely to be the culprit. You need to find out what object is `null`. The linked duplicate has a lot of information about this. – David Aug 16 '17 at 15:39
  • Not sure why the community decided to re-open this question, but it's clearly still a duplicate of: https://stackoverflow.com/questions/4660142/what-is-a-nullreferenceexception-and-how-do-i-fix-it – David Aug 16 '17 at 15:41
  • 2
    Why does the synchronous call have the `async` keyword? – maccettura Aug 16 '17 at 15:43
  • @David our setup does not allow/can't use `BreakPoints` maybe describing it as `WebForms` didn't mean what I thought it would. We make edits to our files and we never hit `Run`. So I'm not sure how else to `Debug`. However, I will test if `CacheName` is `null` before setting the cache to see what happens. – RoLYroLLs Aug 16 '17 at 15:44
  • @RoLYroLLs: If your setup *doesn't allow debugging* then you should probably focus on fixing your setup. It's going to make things *a lot* easier in the future. – David Aug 16 '17 at 15:45
  • @David I totally agree, I've been trying to do that for years. Our site is huge and we haven't had the resources to make the changes. – RoLYroLLs Aug 16 '17 at 15:47
  • 2
    1) Ensure your call stack is all `async` and this is not being called in a "fire and forget" fashion. 2) Ensure [`httpRuntime.targetFramework` is set to `4.5` or newer in `web.config`](https://blogs.msdn.microsoft.com/webdev/2012/11/19/all-about-httpruntime-targetframework/). – Stephen Cleary Aug 16 '17 at 15:50
  • 1
    @L.B wow! You were right. It looks like `HttpContext.Current` was being lost after the async call to `forceClient.DescribeAsync ` – RoLYroLLs Aug 16 '17 at 15:52
  • @RoLYroLLs: If you're not awaiting that call, that's likely the problem. Asynchronous operations should be exposed all the way to the top level so the application context can properly handle them. As Stephen Cleary mentioned previously, you don't want to "fire and forget". This has the tendency of, well, forgetting. – David Aug 16 '17 at 15:53
  • @David you should be able to see my code, I am `awaiting` it – RoLYroLLs Aug 16 '17 at 15:54
  • 1
    @RoLYroLLs: (1) A `NullReferenceException` still is what it is, it's still a possible duplicate. (2) How should I be able to see code you're not showing in the question? If you'd like to add the more complete call stack to the question, then we could see it. But I assure you that nobody here can see how you're calling the `Describe` method until you actually include that in the question. – David Aug 16 '17 at 15:56
  • @David Oh, I understand. Sorry. I'll add my call to it. – RoLYroLLs Aug 16 '17 at 15:57
  • @David It looks like you may be right about the `async` not being set all the way to the top. I added my code with some comments of results when I do. – RoLYroLLs Aug 16 '17 at 17:24
  • @StephenCleary thanks, I updated my question with my full code. However, I get no results when I make changes to my `Page_Load(...)` method. Maybe I need to start figuring this part out first. – RoLYroLLs Aug 16 '17 at 17:25
  • @RoLYroLLs: Did you verify `httpRuntime.targetFramework`? – Stephen Cleary Aug 16 '17 at 19:03
  • @StephenCleary yes I have that specified. Also, I found your blog but didn't find anything helpful for my situation, but I would love to work closer with you if you can to help figure out my true issue(s). – RoLYroLLs Aug 16 '17 at 19:05
  • @RoLYroLLs: Looks like ASP.NET isn't recognizing the `async` method. But if you're running on 4.5 or newer and have `targetFramework` set to an appropriate value (and as long as you aren't disabling the task friendly asp.net context), then it should work. – Stephen Cleary Aug 16 '17 at 19:12
  • @StephenCleary I'm clearly not understanding what's happening. I tested something by creating a blank page: `protected async Task Page_Load(object sender, EventArgs e) { lblTest.Text = "1"; }` does not update the label. Do I need to go into `async` 101 class again? =) – RoLYroLLs Aug 16 '17 at 19:18
  • `Page_Load` is an event, so it needs to be `async void`. – Stephen Cleary Aug 16 '17 at 19:19
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/152089/discussion-between-rolyrolls-and-stephen-cleary). – RoLYroLLs Aug 16 '17 at 19:20
  • @StephenCleary thanks now i get this `An asynchronous operation cannot be started at this time. Asynchronous operations may only be started within an asynchronous handler or module or during certain events in the Page lifecycle. If this exception occurred while executing a Page, ensure that the Page is marked <%@ Page Async="true" %>. This exception may also indicate an attempt to call an "async void" method, which is generally unsupported within ASP.NET request processing. Instead, the asynchronous method should return a Task, and the caller should await it.` was why I went with `async task` – RoLYroLLs Aug 16 '17 at 19:22
  • 1
    @StephenCleary well then, I guess that's my fault. I misunderstood the error. adding `<%@ Page Async="true" %>` worked. {facepalm} – RoLYroLLs Aug 16 '17 at 19:25
  • So not sure how to continue, you want to supply an answer? Do I answer my own question? Issues was having to add `<%@ Page Async="true" %>` when using `protected async void Page_Load(object sender, EventArgs e) {...}` due to `Page_Load` being an event and needing `async void` not `async Task`. – RoLYroLLs Aug 16 '17 at 19:32

2 Answers2

2

Web-related frameworks in .NET generally use thread static variables like HttpContext.Current, OperationContext.Current, WebOperationContext.Current, etc. Since execution may continue in a different thread after an async method call, thread static variables are lost.

Here is a console app just to show what I mean:

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

namespace ConsoleApplication2
{
    class Test
    {
        static void Main()
        {
            MyAsyncMethod().Wait();
            Console.ReadLine();
        }

        [ThreadStatic]
        static int MyContext = 666;

        static async Task MyAsyncMethod()
        {
            MyContext = 555;
            Console.WriteLine(Thread.CurrentThread.ManagedThreadId + " " + MyContext);
            using (var client = new HttpClient())
            {
                var html = await client.GetStringAsync("http://google.com");
            }
            Console.WriteLine(Thread.CurrentThread.ManagedThreadId + " " + MyContext);
        }
    }
}

So, you need to save them before calling an async method:

public static async Task<Models.Description.ObjectDescription> Describe(ForceClient forceClient, string sObject) 
{
    var ctx = HttpContext.Current; //<-- *****
    Models.Description.ObjectDescription result;
    var cacheName = "Salesforce_Object_" + sObject;
    if (ctx.Cache[cacheName] != null) 
    {
        result = (Models.Description.ObjectDescription) ctx.Cache[cacheName];
    } 
    else 
    {
        /*
        * only line that changed from above
        */
        result = await forceClient.DescribeAsync<Models.Description.ObjectDescription>(sObject);

        if (result != null) 
        {
            var expiration = 30; // testing, this will be read from a global variable
            ctx.Cache.Insert(
                cacheName, 
                result, 
                null, 
                DateTime.UtcNow.AddSeconds(expiration), 
                Cache.NoSlidingExpiration, 
                CacheItemPriority.Default, 
                null);
        }
    }
    return result;
}
L.B
  • 114,136
  • 19
  • 178
  • 224
  • 1
    The ASP synchronization context is going to load all of the request's information into the context when posting/sending continuations to run in that context, so you don't need to keep track of them yourself. – Servy Aug 16 '17 at 17:19
  • 1
    @Servy You may be right, but that was not true for WCF sometimes ago. I didn't test it with the current version of .NET – L.B Aug 16 '17 at 17:25
  • 1
    Thanks! This works. However, based on a few comments from @David and @StephenCleary, I may have an `async` problem at the top level which is causing this issue. Let's see how this continues. – RoLYroLLs Aug 16 '17 at 17:29
  • @Servy BTW: I tested it with .Net 4.6.1 + WCF. At least for WCF, what you say in not true for now. – L.B Aug 16 '17 at 17:46
  • @L.B But this is an ASP application, not a WCF application. – Servy Aug 16 '17 at 17:48
  • @Servy I know, But for a similar question for WCF, I'll leave my answer here. Classical answer for "NullReferenceException" doesn't apply here – L.B Aug 16 '17 at 17:50
  • @L.B So you know your answer is wrong for the question asked, but could be useful for entirely separate questions? If that's the case, then post it as an answer to those other questions to which it's a valid answer, rather than to one where it's not. – Servy Aug 16 '17 at 17:51
  • 1
    @Servy have you read RoLYroLLs' comment. (BTW: Why are you so aggresive? You already voted it down.) – L.B Aug 16 '17 at 17:51
  • @L.B The fact that they have the wrong context means that they have a more fundamental problem here than just this one missing value, it means that there code isn't running as a part of the request when it needs to be, and that needs to be fixed; this doesn't fix that. I'm not being aggressive, I'm just pointing out that your answer is wrong, and you've indicated that you know that it's wrong and just don't care, which is rather concerning. – Servy Aug 16 '17 at 17:53
  • @Servy `you've indicated that you know that it's wrong` where? this? `You may be right` – L.B Aug 16 '17 at 17:54
  • @L.B [Right here](https://stackoverflow.com/questions/45717656/losing-variable-after-async-call/45718203?noredirect=1#comment78398348_45718203). – Servy Aug 16 '17 at 17:55
  • @Servy "I know" *question is related with ASP.* – L.B Aug 16 '17 at 17:56
  • @L.B And yet you gave an answer that applies to WCF, not ASP. – Servy Aug 16 '17 at 17:56
  • @Servy And loop starts again – L.B Aug 16 '17 at 17:57
  • @L.B So given that you know that your answer doesn't apply to the framework the OP is using, and only applies to a framework that you know they aren't using, you know the answer is wrong and...don't care. – Servy Aug 16 '17 at 17:59
  • @Servy `you know it's wrong` No I don't know, I haven't test it with ASP. You say so, – L.B Aug 16 '17 at 18:03
  • @L.B So if you posted a C++ answer to a C# question and someone pointed out that this is a C# question, not a C++ answer, your response would be, "I don't know that it's wrong, I haven't tested this answer with C#, you just say it doesn't work in C#."? You know that the answer is for a completely unrelated framework, and that it doesn't apply at all to the question asked. – Servy Aug 16 '17 at 18:15
0

I needed to add the following to my page in order to get it to work:

<%@ Page Async="true" %>

and change the signature of the Page_Load() method from

protected void Page_Load(object sender, EventArgs e) {...}

to

protected async void Page_Load(object sender, EventArgs e) {...}
RoLYroLLs
  • 3,113
  • 4
  • 38
  • 57