Problem
Say we have our beautifully engineered, following SOLID principals, FileStorageService
class which is an implementation of IStorageService
:
public interface IStorageService
{
void WriteAllBytes(string path, byte[] buffer);
byte[] ReadAllBytes(string path);
}
public class FileStorageService : IStorageService
{
public void WriteAllBytes(string path, byte[] buffer)
{
File.WriteAllBytes(path, buffer);
}
public byte[] ReadAllBytes(string path)
{
return File.ReadAllBytes(path);
}
}
What if calling one of the methods resulted in an Exception? As we know reading and writing to files can result in a litany of Exceptions such as: IOException
, DirectoryNotFoundException
, FileNotFoundException
, UnauthorizedAccessException
… and of course the ever possible OutOfMemoryException
(a real concern reading all bytes from a file).
Solution 1 – Not My Problem
We could not care about it, say it is the responsibility of the caller. Problem with that is since the caller is (should be) using an IStorageService
how should it know what Exceptions the run time implementation could throw? Moreover the run time implementation could change in future and as a result the Exceptions it could throw. The lazy solution to this is just to wrap your call in catch-all exception handler:
IStorageService myStorageService = Resolver.Resolve<IStorageService>();
try
{
myStorageService.ReadAllBytes("C:\stuff.data");
}
catch (Exception exception)
{
Logger.Log("Oops something went wrong: " + exception.Message);
}
Hopefully we already know why catching Exception is bad. Also everyone using IStorageService
has to wrap every call in their own Exception handling.
Solution 2 – Create new Exception Type
Perhaps an improvement upon the previous solution would be to create our own Exception Type, say: StorageReadException
which we could throw in our implementation when only an Exception we expect and can deal with occurs. Then the caller can safely only handle StorageReadException
regardless of the of the runtime implementation, e.g.:
public class StorageReadException : Exception
{
public StorageReadException(Exception innerException)
: base(innerException.Message, innerException)
{
}
}
And in our FileStorageService
from earlier:
public byte[] ReadAllBytes(string path)
{
try
{
return File.ReadAllBytes(path);
}
catch (FileNotFoundException fileNotFoundException)
{
throw new StorageReadException(fileNotFoundException);
}
}
And our caller could:
IStorageService myStorageService = Resolver.Resolve<IStorageService>();
try
{
myStorageService.ReadAllBytes(path);
}
catch (StorageReadException sre)
{
Logger.Log(String.Format("Failed to read file from path, {0}: {1}", path, sre.Message));
}
Issues I have with his approach are: we have to create a new Type consumers may be unfamiliar with and the caller still has to write a try catch block every call.
Solution 3 – Try Pattern with Complex Result
IStorageService
operations can fail, i.e. it is not exceptional to fail, let’s alleviate our consumers having to write their own Exception handling every time by using the Try pattern – the same pattern on int
(Int32
) when parsing a string: bool TryParse(string s, out int result)
. We could change
byte[] ReadAllBytes(string path)
to:
bool TryReadAllBytes(string path, out byte[] result)
But that does not give us much information such as why the operation failed; perhaps we want to show a message to the user giving them some information about the failure - and IStorageService
cannot be expected to show a messages as that's not its responsibility. So instead of bool lets’ return a new Type containing: whether the operation was a success, the result of the operation if was successful otherwise a message why not and details about the Exception that caused the failure which the caller can choose to use, introducing OperationResult<TResult>
:
public class OperationResult<TResult>
{
private OperationResult ()
{
}
public bool Success { get; private set; }
public TResult Result { get; private set; }
public string NonSuccessMessage { get; private set; }
public Exception Exception { get; private set; }
public static OperationResult<TResult> CreateSuccessResult(TResult result)
{
return new OperationResult<TResult> { Success = true, Result = result};
}
public static OperationResult<TResult> CreateFailure(string nonSuccessMessage)
{
return new OperationResult<TResult> { Success = false, NonSuccessMessage = nonSuccessMessage};
}
public static OperationResult<TResult> CreateFailure(Exception ex)
{
return new OperationResult<TResult>
{
Success = false,
NonSuccessMessage = String.Format("{0}{1}{1}{2}", ex.Message, Environment.NewLine, ex.StackTrace),
Exception = ex
};
}
}
(I used a private constructor so one of the Create()
methods have to be used ensuring Result
has to be set if successful; otherwise NonSuccessMessage
has to be supplied if not). We can change FileStorageService
’s ReadAllBytes()
method (and the interface) to:
public OperationResult<byte[]> TryReadAllBytes(string path)
{
try
{
var bytes = File.ReadAllBytes(path);
return OperationResult<byte[]>.CreateSuccessResult(bytes);
}
catch (FileNotFoundException fileNotFoundException)
{
return OperationResult<byte[]>.CreateFailure(fileNotFoundException);
}
}
And the calling code becomes:
var result = myStorageService.TryReadAllBytes(path);
if(result.Success)
{
}
else
{
Logger.Log(String.Format("Failed to read file from path, {0}: {1}", path, result.NonSuccessMessage));
}
That’s it! Now if an Exception we cannot handle occurs it bubbles as it should; if an expected Exception occurred in the implementation we can use the information on the returned OperationResult
.
OperationResult<TResult>
can be used across the API on all operations that can fail.
History
27 August 2015 - Initial Version