1

I've got an issue calling a method that may return a Task<T> or null depending on the result of an initial synchronous lookup call (this itself might be an anti-pattern so please let me know).

I kind of want to return null if a trivial exit condition occurs but this is causing the calling (outer) method to fail because the outer call expects a Task<T> response (trivial exit) which gets pushed through to ConfigureAwait(true) which subsequently produces a NullReferenceException.

Outer calling method:

var res = await MyService.GetUserCourseStatusAsync(userID, productID).ConfigureAwait(true);

Intermediate method:

public Task<IGetUserCourseResponse> GetUserCourseStatusAsync(int userID, int productID)
{
    // Look up User's ID (if exists)
    var userCredentials = GetUserCredentials(userID);

    if (userCredentials?.UserID == null)
        return null; // Trivial return of null ("User not registered"). *** This causes exception on ConfigureAwait(true) above ***

    // Another synchronous call
    var courseId = GetCourseID(productID);

    if (courseId == null)
        throw new InvalidOperationException($"Product #{productID} is not a course");

    // Asynchronous call to inner method (this bit works fine)
    return GetUserCourseAsync(userCredentials.UserID.Value, courseId.Value);
}

So my thought then is that we should always return a Task<T> instead of null. However, all of these cause compile errors:

//return null; // Trivial return of null ("User not registered"). *** This causes exception 

// Compile error: CS0029: Cannot implicitly convert type 'GetUserCourseInner' to 'System.Threading.Tasks.Task<IGetUserCourseResponse>'
return new GetUserCourseInner(); // Not registered

// Compile error: CS1503    Argument 1: cannot convert from 'GetUserCourseInner' to 'System.Func<IGetUserCourseResponse>'
return new Task<IGetUserCourseResponse>(new GetUserCourseInner()); // Not registered

How do I return a dummy Task<T> that isn't a result of a async call?

Is this even the correct approach?

Dmitry Arestov
  • 1,427
  • 12
  • 24
Chris Walsh
  • 3,423
  • 2
  • 42
  • 62

2 Answers2

4

It would be better, as you suggested, to return a Task<IGetUserCourseResponse> which contains null (or some other sentinel value). You can create such a completed Task with Task.FromResult((IGetUserCourseResponse)null):

public Task<IGetUserCourseResponse> GetUserCourseStatusAsync(int userID, int productID)
{
    // Look up User's ID (if exists)
    var userCredentials = GetUserCredentials(userID);
    if (userCredentials?.UserID == null)
        return Task.FromResult((IGetUserCourseResponse)null);

    // Another synchronous call
    var courseId = GetCourseID(productID);
    if (courseId == null)
        throw new InvalidOperationException($"Product #{productID} is not a course");

    // Asynchronous call to inner method (this bit works fine)
    return GetUserCourseAsync(userCredentials.UserID.Value, courseId.Value);
}

Alternatively, you could make your outer method async. Note however that this changes its behaviour in the case where it throws an InvalidOperationException: instead of the method throwing this exception directly, it will instead return a Task which contains this exception. This may or may not be what you want:

public async Task<IGetUserCourseResponse> GetUserCourseStatusAsync(int userID, int productID)
{
    // Look up User's ID (if exists)
    var userCredentials = GetUserCredentials(userID);
    if (userCredentials?.UserID == null)
        return null;

    // Another synchronous call
    var courseId = GetCourseID(productID);
    if (courseId == null)
        throw new InvalidOperationException($"Product #{productID} is not a course");

    // Asynchronous call to inner method (this bit works fine)
    return await GetUserCourseAsync(userCredentials.UserID.Value, courseId.Value);
}
canton7
  • 37,633
  • 3
  • 64
  • 77
  • Brilliant. Thanks canton7 and thanks for the alternative option and gotcha. Most appreciated. – Chris Walsh Jan 06 '20 at 15:10
  • 3
    @ChrisWalsh: I'd recommend the solution using `async`. The *normal* way to raise exceptions from asynchronous methods is to place them on the returned promise, which is what `async` will do for you. An asynchronous method that throws an exception directly on the call stack is implementing unexpected behavior. – Stephen Cleary Jan 06 '20 at 15:23
  • Thanks Stephen. I'll do this. – Chris Walsh Jan 06 '20 at 18:43
3

Just return a Task that holds a null value as result

return Task.FromResult<IGetUserCourseResponse>(null);
MikNiller
  • 1,242
  • 11
  • 17
  • Brilliant. Thanks. I thought it would something like this but I've not had this case come up yet and still learning the ropes. I'll check canton7's answer below too and select the most appropriate as the answer. Thanks. – Chris Walsh Jan 06 '20 at 15:09