This question already has answers here:
Closed 10 months ago.
I often use the TryDoSomething
pattern like this totally made up example:
GameContext gameContext;
if (gamesRepository.TryLoadLastGame(out gameContext))
{
// Perform actions with gameContext instance.
}
else
{
// Create a new game or go to home screen or whatever.
}
This allows a nice readable flow but it also allows a success status true
but a null return value which is useful sometimes for communicating "I was able to get you your thing but its actually null".
With async-await, the asynchronous lower APIs "force" calling APIs to do the right thing and work asynchronously. However, without out
parameters, this pattern doesn't work.
How can it be achieved?
I have an answer, and am answering Q/A style to see how other people think about it.
So far I've used a little class called Attempt<T>
:
public sealed class Attempt<T>
{
/// <summary>
/// Initializes a new instance of the <see cref="Attempt{T}"/> class.
/// </summary>
public Attempt() { }
/// <summary>
/// Initializes a new instance of the <see cref="Attempt{T}"/> class.
/// </summary>
public Attempt(Exception exception)
{
this.Exception = exception;
}
/// <summary>
/// Initializes a new instance of the <see cref="Attempt{T}"/> class.
/// </summary>
/// <param name="result">The result.</param>
public Attempt(T result)
{
this.Result = result;
this.HasResult = true;
}
/// <summary>
/// Gets the result.
/// </summary>
/// <value>The result.</value>
public T Result { get; private set; }
/// <summary>
/// Determines whether this instance has result.
/// </summary>
/// <returns><c>true</c> if this instance has result; otherwise, <c>false</c>.</returns>
public bool HasResult { get; private set; }
/// <summary>
/// Returns the result with a true or false depending on whether its empty, but throws if there is an exception in the attempt.
/// </summary>
/// <param name="result">The result, which may be null.</param>
/// <returns><c>true</c> if the specified result has a value; otherwise, <c>false</c>.</returns>
/// <exception cref="System.AggregateException">The attempt resulted in an exception. See InnerExceptions.</exception>
public bool TryResult(out T result)
{
if (this.HasResult)
{
result = this.Result;
return true;
}
else
{
if (this.Exception != null)
{
throw new AggregateException("The attempt resulted in an exception. See InnerExceptions.", this.Exception);
}
else
{
result = default(T);
return false;
}
}
}
/// <summary>
/// Gets or sets the exception.
/// </summary>
/// <value>The exception.</value>
public Exception Exception { get; private set; }
}
Which is used within a Try method like this:
internal async Task<Attempt<T>> TryReadObjectAsync<T>(string folderName, string fileName)
{
if (String.IsNullOrWhiteSpace(folderName))
throw new ArgumentException("The string argument is null or whitespace.", "folderName");
if (String.IsNullOrWhiteSpace(fileName))
throw new ArgumentException("The string argument is null or whitespace.", "fileName");
try
{
StorageFolder folder = this.StorageFolder;
if (folderName != @"\")
folder = await StorageFolder.GetFolderAsync(folderName);
var file = await folder.GetFileAsync(fileName);
var buffy = await FileIO.ReadBufferAsync(file);
string xml = await buffy.ReadUTF8Async();
T obj = await Evoq.Serialization.DataContractSerializerHelper.DeserializeAsync<T>(xml);
return new Attempt<T>(obj);
}
catch (FileNotFoundException fnfe)
{
// Only catch and wrap expected exceptions.
return new Attempt<T>(fnfe);
}
}
And is consumed like so:
DataContractStorageSerializer xmlStorage = new DataContractStorageSerializer(this.StorageFolder);
var readAttempt = await xmlStorage.TryReadObjectAsync<UserProfile>(userName, UserProfileFilename);
try
{
UserProfile user;
if (readAttempt.TryResult(out user))
{
// Do something with user.
}
else
{
// No user in the persisted file.
}
}
catch (Exception ex)
{
// Some unexpected, unhandled exception happened.
}
Or, ignoring errors.
...
var readAttempt = await xmlStorage.TryReadObjectAsync<UserProfile>(userName, UserProfileFilename);
if (readAttempt.HasResult)
{
// Continue.
this.DoSomethingWith(readAttempt.Result);
}
else
{
// Create new user.
}
For comparison purposes, here's a similar approach that I employ:
/// <summary>
/// Encapsulates the return value from a method that has a value, error status and optional error message.
/// This is to be used in cases where a method call can fail, but you don't want to throw an exception;
/// for example, because you have an error message string that you want to be displayed to the user.
/// </summary>
/// <typeparam name="T">The type of the main return value.</typeparam>
[DataContract]
public class Result<T>
{
/// <summary>Creates a failure result without an error message or an exception.</summary>
/// <returns>A failure result value.</returns>
public static Result<T> Failure()
{
return new Result<T>(default(T), false, string.Empty, null, false);
}
/// <summary>Creates a failure result with an error message but no exception.</summary>
/// <param name="errorMessage">
/// The optional error message. This may not be null.
/// It may however be <see cref="String.Empty"/>.
/// </param>
/// <returns>A failure result value.</returns>
public static Result<T> Failure(string errorMessage)
{
return new Result<T>(default(T), false, errorMessage, null, false);
}
/// <summary>Creates a failure result with an exception but no error message.</summary>
/// <param name="exception">The exception. This may be null.</param>
/// <returns>A failure result value.</returns>
public static Result<T> Failure(Exception exception)
{
return new Result<T>(default(T), false, string.Empty, exception, false);
}
/// <summary>Creates a failure result with an error message and an exception.</summary>
/// <param name="errorMessage">
/// The optional error message. This may not be null.
/// It may however be <see cref="String.Empty"/>.
/// </param>
/// <param name="exception">The exception. This may be null.</param>
/// <returns>A failure result value.</returns>
public static Result<T> Failure(string errorMessage, Exception exception)
{
return new Result<T>(default(T), false, errorMessage, exception, false);
}
/// <summary>Creates a failure result without an error message or an exception.</summary>
/// <param name="value">The result value.</param>
/// <returns>A failure result value.</returns>
public static Result<T> Failure(T value)
{
return new Result<T>(value, false, string.Empty, null, true);
}
/// <summary>Creates a failure result with an error message but no exception.</summary>
/// <param name="errorMessage">
/// The optional error message. This may not be null.
/// It may however be <see cref="String.Empty"/>.
/// </param>
/// <param name="value">The result value.</param>
/// <returns>A failure result value.</returns>
public static Result<T> Failure(string errorMessage, T value)
{
return new Result<T>(value, false, errorMessage, null, true);
}
/// <summary>Creates a failure result with an exception but no error message.</summary>
/// <param name="exception">The exception. This may be null.</param>
/// <param name="value">The result value.</param>
/// <returns>A failure result value.</returns>
public static Result<T> Failure(Exception exception, T value)
{
return new Result<T>(value, false, string.Empty, exception, true);
}
/// <summary>Creates a failure result with an error message and an exception.</summary>
/// <param name="errorMessage">
/// The optional error message. This may not be null.
/// It may however be <see cref="String.Empty"/>.
/// </param>
/// <param name="exception">The exception. This may be null.</param>
/// <param name="value">The result value.</param>
/// <returns>A failure result value.</returns>
public static Result<T> Failure(string errorMessage, Exception exception, T value)
{
return new Result<T>(value, false, errorMessage, exception, true);
}
/// <summary>Creates a success result.</summary>
/// <param name="value">The successful value.</param>
/// <returns>A success result value.</returns>
public static Result<T> Success(T value)
{
return new Result<T>(value, true, string.Empty, null, true);
}
/// <summary>
/// The error message, if applicable and if <see cref="IsSuccessful"/> is false.
/// This cannot be null, but it may be <see cref="String.Empty"/>.
/// This is meaningless if <see cref="IsSuccessful"/> is true.
/// Do not call this if <see cref="IsSuccessful"/> is true.
/// </summary>
/// <exception cref="InvalidOperationException">
/// Thrown if this is called when <see cref="IsSuccessful"/> is true.
/// </exception>
public string ErrorMessage
{
get
{
if (IsSuccessful)
{
throw new InvalidOperationException("You cannot access the error message if the method call was successful.");
}
return _errorMessage;
}
}
/// <summary>
/// The primary return value from the method.
/// This is meaningless if <see cref="HasValue"/> is false.
/// Do not call this if <see cref="HasValue"/> is false.
/// (If <see cref="IsSuccessful"/> is true, then <see cref="HasValue"/> will also be true.)
/// </summary>
/// <exception cref="InvalidOperationException">
/// Thrown if this is called when <see cref="HasValue"/> is false.
/// </exception>
public T Value
{
get
{
if (!HasValue)
{
throw new InvalidOperationException("You cannot access the result value if the method call failed and did not set a result value.");
}
return _value;
}
}
/// <summary>
/// Does the result have a valid value?
/// If <see cref="IsSuccessful"/> is true then this will also be true.
/// A result cannot be successful without having an associated valid result value.
/// </summary>
public bool HasValue
{
get
{
return _hasValue;
}
}
/// <summary>
/// Was the method call successful?
/// If this is true, then <see cref="HasValue"/> will also be true.
/// A result cannot be successful without having an associated valid result value.
/// </summary>
public bool IsSuccessful
{
get
{
return _isSuccessful;
}
}
/// <summary>
/// The optional <see cref="Exception"/> associated with a failure, if applicable.
/// This may be null.
/// Do not call this if <see cref="IsSuccessful"/> is true.
/// </summary>
/// <exception cref="InvalidOperationException">
/// Thrown if this is called when <see cref="IsSuccessful"/> is true.
/// </exception>
public Exception Exception
{
get
{
if (IsSuccessful)
{
throw new InvalidOperationException("You cannot access the exception if the method call was successful.");
}
return _exception;
}
}
/// <summary>Constructor.</summary>
/// <param name="value">T</param>
/// <param name="isSuccessful">Was the method call successful?</param>
/// <param name="errorMessage">The optional error message. This may not be null.
/// It must be <see cref="string.Empty"/> if <paramref name="isSuccessful"/> is true.</param>
/// <param name="exception">The exception, if applicable. Must be null if <paramref name="isSuccessful"/> is true.</param>
/// <param name="isValueValid">Is <paramref name="value"/> valid?</param>
private Result(T value, bool isSuccessful, string errorMessage, Exception exception, bool isValueValid)
{
if (errorMessage == null)
{
throw new ArgumentNullException("errorMessage");
}
if (isSuccessful && !string.IsNullOrEmpty(errorMessage))
{
throw new ArgumentOutOfRangeException("errorMessage", errorMessage, "errorMessage must be empty if isSuccessful is true.");
}
if (isSuccessful && (exception != null))
{
throw new ArgumentOutOfRangeException("exception", exception, "exception must be null if isSuccessful is true.");
}
_value = value;
_exception = exception;
_isSuccessful = isSuccessful;
_hasValue = isValueValid;
_errorMessage = errorMessage;
}
[DataMember] private readonly T _value;
[DataMember] private readonly string _errorMessage;
[DataMember] private readonly bool _isSuccessful;
[DataMember] private readonly bool _hasValue;
[DataMember] private readonly Exception _exception;
}
/// <summary>
/// Encapsulates the return value from a method that has an error status and optional error message,
/// but no actual return value.
/// This is to be used in cases where a method call can fail, but you don't want to throw an exception;
/// for example, because you have an error message string that you want to be displayed to the user.
/// </summary>
[DataContract]
public class Result
{
/// <summary>Creates a failure result without an error message or an exception.</summary>
/// <returns>A failure result value.</returns>
public static Result Failure()
{
return new Result(false, string.Empty, null);
}
/// <summary>Creates a failure result with an error message but no exception.</summary>
/// <param name="errorMessage">
/// The optional error message. This may not be null.
/// It may however be <see cref="String.Empty"/>.
/// </param>
/// <returns>A failure result value.</returns>
public static Result Failure(string errorMessage)
{
return new Result(false, errorMessage, null);
}
/// <summary>Creates a failure result with an exception but no error message.</summary>
/// <param name="exception">The exception. This may be null.</param>
/// <returns>A failure result value.</returns>
public static Result Failure(Exception exception)
{
return new Result(false, string.Empty, exception);
}
/// <summary>Creates a failure result with an error message and an exception.</summary>
/// <param name="errorMessage">
/// The optional error message. This may not be null.
/// It may however be <see cref="String.Empty"/>.
/// </param>
/// <param name="exception">The exception. This may be null.</param>
/// <returns>A failure result value.</returns>
public static Result Failure(string errorMessage, Exception exception)
{
return new Result(false, errorMessage, exception);
}
/// <summary>Creates a success result.</summary>
/// <returns>A success result value.</returns>
public static Result Success()
{
return new Result(true, string.Empty, null);
}
/// <summary>
/// The error message, if applicable and if <see cref="IsSuccessful"/> is false.
/// This cannot be null, but it may be <see cref="String.Empty"/>.
/// This is meaningless if <see cref="IsSuccessful"/> is true.
/// Do not call this if <see cref="IsSuccessful"/> is true.
/// </summary>
/// <exception cref="InvalidOperationException">
/// Thrown if this is called when <see cref="IsSuccessful"/> is true.
/// </exception>
public string ErrorMessage
{
get
{
if (IsSuccessful)
{
throw new InvalidOperationException("You cannot access the error message if the method call was successful.");
}
return _errorMessage;
}
}
/// <summary>Was the method call successful?</summary>
public bool IsSuccessful
{
get
{
return _isSuccessful;
}
}
/// <summary>
/// The optional <see cref="Exception"/> associated with a failure, if applicable.
/// This may be null.
/// Do not call this if <see cref="IsSuccessful"/> is true.
/// </summary>
/// <exception cref="InvalidOperationException">
/// Thrown if this is called when <see cref="IsSuccessful"/> is true.
/// </exception>
public Exception Exception
{
get
{
if (IsSuccessful)
{
throw new InvalidOperationException("You cannot access the exception if the method call was successful.");
}
return _exception;
}
}
/// <summary>Constructor.</summary>
/// <param name="isSuccessful">Was the method call successful?</param>
/// <param name="errorMessage">The optional error message. This may not be null.
/// It must be <see cref="string.Empty"/> if <paramref name="isSuccessful"/> is true.</param>
/// <param name="exception">The exception, if applicable. Must be null if <paramref name="isSuccessful"/> is true.</param>
private Result(bool isSuccessful, string errorMessage, Exception exception)
{
if (errorMessage == null)
{
throw new ArgumentNullException("errorMessage");
}
if (isSuccessful && !string.IsNullOrEmpty(errorMessage))
{
throw new ArgumentOutOfRangeException("errorMessage", errorMessage, "errorMessage must be empty if isSuccessful is true.");
}
if (isSuccessful && (exception != null))
{
throw new ArgumentOutOfRangeException("exception", exception, "exception must be null if isSuccessful is true.");
}
_exception = exception;
_isSuccessful = isSuccessful;
_errorMessage = errorMessage;
}
[DataMember] private readonly string _errorMessage;
[DataMember] private readonly bool _isSuccessful;
[DataMember] private readonly Exception _exception;
}