0

It seems that in a Blazor Server app we are encouraged to use Async where possible, and I generally understand why. That said - would someone please be kind enough to explain the general expectation when using Async functions and fill in some missing knowledge for me - I'm confused by all the information that's out there and need something specific to what I'm doing, to help me understand.

I'm trying to stick to using async where possible and in most cases it's fairly easy to do so, but when calling some functions, it seems overkill to make them asynchronous (or is it?). In the the example below - the NON-async 'ValidateEvent' function is being called from within an async function.

So my question is ...Should I:

a) Call it normally from within the Async function (which seems to defeat the point of async) eg: "var validationResult = ValidateEvent(objDto);"?

b) Call it using Task.Run eg: "await Task.Run(() =>ValidateEvent(objDto));"?

c) Convert this simple IF/ELSE method into an Async function?

Thanks in advance for any help/advice.

//example async function, that itself calls the other non-async function.
public async Task<Response> AddAsync(ObjDto objDto)
{
  // a) Call it normally?
  var validationResult = ValidateEvent(objDto);

  // b) Calling it using Task.Run?
  var validationResult = await Task.Run(() =>ValidateEvent(objDto));


  //Do stuff asynchronously here
  ...
  await _db.AddAsync(objDto);
  await _db.SaveChangesAsync();
  ...
}

The validation function:

c) Should I really be converting this to async since it's just a series of 'IFs and ELSEs' (async conversion further below)

//Non-Async version
public ResponseObj ValidateEvent(ObjDto obj)
 {
   ResponseObj responseObj = new();
   string stringErrors = "";

   //If not full day, check that end date is not before start date
   if (!obj.IsFullDay)
   {
     if (obj.EndTime < obj.StartTime)
     stringErrors += ("End date cannot be before the start date<br />");
   }
   ...other code removed for brevity
   if (string.IsNullOrEmpty(stringErrors)) //Success
   {
     responseObj.Status = responseObj.Success;
   }
   else //errors
  {
    responseObj.Status = responseObj.Error;
    responseObj.Message = stringErrors;
  }

  return responseObj;
 }

//Example Async conversion - is it worth converting this using Task.Run?

//Async conversion
public async Task<ResponseObj> ValidateEvent(ObjDto obj)
{
    ResponseObj responseObj = new();
    string stringErrors = "";
    
   await Task.Run(() =>
       {
           //If not full day, check that end date is not before start date
           if (!obj.IsFullDay)
           {
               if (obj.EndTime < obj.StartTime)
                   stringErrors += ("End date cannot be before the start date<br />");
           }

           if (string.IsNullOrEmpty(stringErrors)) //Success
           {
               responseObj.Status = responseObj.Success;
           }
           else //errors
           {
               responseObj.Status = responseObj.Error;
               responseObj.Message = stringErrors;
           }

       }
   );
   return responseObj;
}

Again, thanks in advance for any help/advice in understaning the best way to go with this in general.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
MRB
  • 43
  • 1
  • 6
  • 1
    Yes, it's overkill. – Enigmativity Sep 09 '21 at 06:12
  • 5
    "It seems that we are encouraged to use Async where possible" - No, I don't think so. Why do you say so? – Enigmativity Sep 09 '21 at 06:13
  • 3
    No-one is telling you to convert non IO bound workloads to the async and await pattern. – TheGeneral Sep 09 '21 at 06:15
  • 1
    Where an async method is the callee, the caller should normally also be async. The inverse isn't necessarily true, and certainly isn't in the case of your validation method. – ProgrammingLlama Sep 09 '21 at 06:16
  • 1
    Having nested async calls doesn't help increasing the responsiveness of the top level code. It just increases the internal task overhead. – PMF Sep 09 '21 at 06:16
  • 2
    The decision to go async generally flows *up* the call stack. You don't normally force it *down* into called functions. – Damien_The_Unbeliever Sep 09 '21 at 06:17
  • I should have pointed out that I'm using (learning) Blazor, and await/async is heavily used in tuturials and code examples, so I'm simply trying to understand where to draw the line - and when to accept synchronous as being "ok" to use. Thanks – MRB Sep 09 '21 at 06:19
  • In Blazor-Wasm you can't even use Task.Run(). Don't overthink it, just use async when appropriate. – H H Sep 09 '21 at 06:27

2 Answers2

5

a) Call it normally from within the Async function ...

Yes

... (which seems to defeat the point of async)

No it doesn't.

H H
  • 263,252
  • 30
  • 330
  • 514
2

It depends on what the ValidateEvent method does.

  1. In case it does a trivial amount of work that is not going to be increased over time, like your example suggests, then it's completely OK to invoke it from inside the asynchronous AddAsync method.

  2. In case it blocks for a considerable amount of time, say for more than 50 msec, then invoking it from inside an asynchronous method would violates Microsoft's guidelines about how asynchronous methods are expected to behave:

An asynchronous method that is based on TAP can do a small amount of work synchronously, such as validating arguments and initiating the asynchronous operation, before it returns the resulting task. Synchronous work should be kept to the minimum so the asynchronous method can return quickly.

So what you should do if the ValidateEvent method blocks, and you can't do anything about it? AFAIK you are in a gray area, with all options being unsatisfactory for one reason or another.

  1. You can leave it as is, accepting that the method violates Microsoft's guidelines, document this behavior, and leave to the callers the option to wrap the AddAsync in a Task.Run, if this is beneficial for them.

  2. You can wrap the ValidateEvent in a Task.Run, like you did in your question. This violates (partially) another guideline by Microsoft: Do not expose asynchronous wrappers for synchronous methods.

  3. You can split your AddAsync method into two parts, the synchronous ValidateAdd and the asynchronous AddAsync, expecting that the callers will invoke the one method after the other.

If your AddAsync method is part of an application-agnostic library, I would say go with the option 1. That's what Microsoft is doing with some built-in APIs. If the AddAsync method is application-specific, and the application has a GUI (WinForms, WPF etc), then it is tempting to go with the option 2. Keeping the UI responsive is paramount for a GUI application, so the Task.Run should be included somewhere, and putting it inside the AddAsync seems like a convenient place to put it. The option 3 is probably the least attractive option for this particular case, because validating and adding are tightly coupled operations, but it may be a good option for other scenarios.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
  • 1
    That's really, helpful and useful information, thank you so much for taking the time to explain this clearly. – MRB Sep 09 '21 at 09:38