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?
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);
}