0

I am writing a Web API project and for one of my end points (the HandleAsync method is the implementation) I am unable to use the discard syntax (_) and I can't figure out why.

I have other places that do this same pattern: call CheckoutFileAsync and in the processNotifications delegate I am able to use _ fine when calling SendEmailAsync.

The only difference is a different entry of IfContentManagementAuthorizedAsync instead of IfFileActionAuthorizedAsync, but each has the 'same' concept of an async callback if everything is good in the authorization logic.

The compile error I receive is on this line

_ = emailService.SendEmailAsync(

and this is the error:

Cannot implicitly convert type 'System.Threading.Tasks.Task' to 'bool'

And if I hover over the _, intellisense says (parameter) bool _. If I hover over _ where this works, it says (discard) Task<bool> _.

I basically want a 'fire and forget' mechanism to send out a notification email. I don't want to process the results or wait for any delays that might occur and as I understood it, the discard syntax is the pattern I should be doing. Given the code below, do you see any reason why I couldn't use the discard syntax?

Note, I have two overloads of If*AuthorizedAsync methods, one returning Task<IActionResult> and the other returning Task<ActionResult<T>> because they are used in all my end points which vary their return type and I couldn't figure out how to make one signature work for all end points.

public async Task<IActionResult> HandleAsync( [FromQuery] FileIdParameters parameters )
{
    var fileInfo = await GetFilePropertiesAsync<DataLockerFile>( parameters.Id, null );

    if ( fileInfo == null )
    {
        return NotFound();
    }

    return await IfFileActionAuthorizedAsync(
        "Write",
        fileInfo.FolderName,
        async ( user, userEmail, canCreateFolder ) =>
            await CheckoutFileAsync(
                userEmail,
                fileInfo,
                dateTimeService.UtcNow,
                fileInfo =>
                {
                    _ = emailService.SendEmailAsync(
                        string.Join( ";", notificationEmails ),
                        emailService.DefaultFrom,
                        $"KAT Management Secure File Checkout: {fileInfo.FolderName}:{fileInfo.Name}",
                        $"{user} has checked out {fileInfo.FolderName}:{fileInfo.Name}."
                    );
                }
            )
    );
}

protected async Task<ActionResult<T>> IfFileActionAuthorizedAsync<T>(
    string requiredClaim, string folder,
    Func<string, string, bool, Task<ActionResult<T>>> validResult )
{   
    var claimResult = Foo(); // .. logic 
    return await validResult( claimResult.User, claimResult.UserEmail, claimResult.CanCreateFolder );
}
protected async Task<IActionResult> IfFileActionAuthorizedAsync(
    string requiredClaim, string folder,
    Func<string, string, bool, Task<IActionResult>> validResult )
{   
    var claimResult = Foo(); // .. logic 
    return await validResult( claimResult.User, claimResult.UserEmail, claimResult.CanCreateFolder );
}

protected async Task<IActionResult> CheckoutFileAsync(
    string userEmail,
    DataLockerBaseFile fileInfo,
    DateTime checkoutTime,
    Action<DataLockerBaseFile> processNotifications
)
{
    var fileInfo = await DoSomethingAsync(); // logic

    processNotifications( fileInfo );

    return Ok();
}

// This method is part of another class, the smtp configuration object is inject with DI
public async Task<bool> SendEmailAsync( string to, string from, string subject, string body )
{
    var message = CreateMailMessage( to, from, subject body );

    var mailClient = new SmtpClient( smtp.Server );
    mailClient.Credentials = new System.Net.NetworkCredential( smtp.UserName, smtp.Password );
    await mailClient.SendMailAsync( message );

    return true;
}
Terry
  • 2,148
  • 2
  • 32
  • 53
  • [Fire and Forget](https://stackoverflow.com/questions/1018610/simplest-way-to-do-a-fire-and-forget-method-in-c). – Robert Harvey Apr 24 '21 at 12:59
  • What @RobertHarvey said, but why not just `emailService.SendEmailAsync` (without the `_ =)`? – tymtam Apr 24 '21 at 13:15
  • You show us `SendEmailAsync ` accepting 3 params but it's called with 4 params. – tymtam Apr 24 '21 at 13:27
  • @tymtam Mostly consistency. When I removed from this location (if that is even the right pattern, as I said, I think I've read you should use `_`), I removed it from other location as well, but then compiler complained there that I *should* use the `_`. Not too experienced in tasked based programming and want to do it 'right' **and** consistent in all locations. Maybe naive...so I should try changing all locations to `Task.Run( () => emailService.SendEmailAsync() )`? (param count was lazy psuedo code for brevity, fixing) – Terry Apr 24 '21 at 13:31
  • Is this the only build error? – tymtam Apr 24 '21 at 13:37
  • Yes, only build error. – Terry Apr 24 '21 at 13:37
  • Ok, so there is a difference between this spot and the others. Why would the compiles require it? Hm... – tymtam Apr 24 '21 at 13:40
  • 2
    @tymtam OK, this is **nuts**. I had restarted PC/Visual Studio and problem remained. I was 'giving up' so I could move on and changed `CheckoutFileAsync` to have this as callback signature `Func processNotifications` so that I could have an `async` delegate and use `await` on the call to `SendEmailAsync`. Changed all locations, everything worked. Then to confirm your questions, I went back to signature above and was going to get the compiler warning without `_`, but now EVERYTHING COMPILES as written above with `_` in all the locations. Don't know what to say. – Terry Apr 24 '21 at 13:46
  • This **was** nuts. It's working and we can move on. – tymtam Apr 24 '21 at 13:49
  • After this exercise in insanity, I read something, that makes me wonder if I should be using `await` all along. `emailService` is a 'Scoped' dependency injected by DI. I was reading that there is possibilities that this could be 'destroyed' before it is processed since I'm doing a 'fire and forget'? – Terry Apr 24 '21 at 13:51
  • Yes, there is no guarantee that it will be done. Imagine the snippet from my answer, without `ReadLine` - email will not be sent. – tymtam Apr 24 '21 at 14:00
  • I have seen myself crazy things happening with the latest versions of Visual Studio 2019 (version 16.8 and later). In some occasions the editor starts emitting imaginary compile errors for code that no longer exists. The VS is also unusable for more than a minute after startup in my slow machine, and at random moments afterwards, by clogging the CPU and being non-responsive. Apparently this software is developed on high-end machines, and no one tries it on low-capabilities machines to see how it looks like. – Theodor Zoulias Apr 24 '21 at 14:20

1 Answers1

3

The IntelliSense hovering indicates the problem:

And if I hover over the _, intellisense says (parameter) bool _.

The code causing this issue is using a common pattern for sort-of-but-not-really-discarding parameter values, one of which is a bool. E.g., it probably has some lambda of the form (arg, _ /* unneeded boolean argument */) => { ... }. On a side note, the code you posted does not have this pattern and does not cause the problem; when simplifying code to post, please ensure the code you end up posting actually reproduces the problem.

This is a common pattern to indicate "this parameter isn't needed"; however, it's not an actual discard at the language level (except in certain cases in C# 9.0). Instead, it actually defines a parameter named _ of type bool.

So, the discard pattern _ = ... doesn't work for that code block. Since there's an actual parameter named _, the compiler treats it as an assignment to that parameter, and gives the error:

Cannot implicitly convert type 'System.Threading.Tasks.Task' to 'bool'

The solution is to rename the sort-of-discard parameter from _ to something like __. Then the discard pattern will work.

On a side note, "fire-and-forget" is almost always the wrong solution.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • 1
    EXACTLY! You are too smart :) I wanted to make it easy to 'reference' the `_` in my question without confusion. I did originally have `async ( user, userEmail, _ ) =>` and I changed it after posting into question not knowing that was an issue. Thanks. – Terry Apr 24 '21 at 14:03
  • Based on your recommendation to 'never' do Fire and Forget, I'm changing to just `await` my `SendEmailAsync`. I just changed `fileInfo =>` to `async fileInfo =>` so I could `await` the call. I'm surprised I didn't have to change my `Action processNotifications` signature to `Func processNotifications`. Why would I be able to add `async` to the delegate when it is defined as `Action<>`? – Terry Apr 24 '21 at 14:09
  • 1
    An `async` `Action` delegate is dangerous: you end up with an `async void` method, which [should be avoided](https://learn.microsoft.com/en-us/archive/msdn-magazine/2013/march/async-await-best-practices-in-asynchronous-programming). The `Action` should be changed to a `Func`. – Stephen Cleary Apr 24 '21 at 14:31