Often during the course of developing software, you will encounter scenarios where you will need to handle the outcome of an operation or task. Whether it's an API call, a database query, or a complex business logic, the result of these operations can determine the flow of our application. To make our code more maintainable and readable, we can leverage the Operation Result pattern.
In this post, we'll dive into the world of the Operation Result pattern, explore its benefits, and see how to implement it in your C# projects.
What is the Operation Result Pattern?
The Operation Result pattern, which is also commonly known as or referred to as the Result pattern or Outcome pattern, is a software design pattern that encapsulates the result of an operation or task, to provide a clean and concise way to handle success and failure scenarios, making your code more robust and easier to maintain.
The Operation Result pattern has its roots in various programming languages and paradigms, and although difficult to pinpoint its exact origins, we can determine a number of influences and scenarios that contributed to the development of this pattern.
- Functional Programming: In functional programming languages like Haskell, Lisp, and Scala, the concept of returning a result or outcome from a function is fundamental. This approach emphasizes the use of immutable data structures and encourages the separation of concerns between the operation and its outcome.
- Error Handling in C: In the C programming language, error handling is often implemented using error codes, which are returned by functions to indicate success or failure. This approach is similar to the Operation Result Pattern, where an error object is returned to represent an unsuccessful operation.
- Exception Handling in Java and C#: Java and C# introduced exception handling mechanisms, which allow for more robust error handling. While not directly related to the Operation Result Pattern, these mechanisms share similarities with the pattern's approach to handling errors.
- Rust's Error Handling: The Rust programming language has a strong focus on error handling and provides a built-in Result type that encapsulates the outcome of an operation. Rust's approach to error handling has influenced the development of the Operation Result Pattern.
- Domain-Driven Design (DDD): DDD, a software development approach, emphasizes the importance of modeling business domains and handling errors in a structured way. The Operation Result Pattern can be seen as a practical implementation of DDD principles in the context of error handling.
The pattern has been widely adopted in modern software development to provide a robust and maintainable way to handle the outcome of operations which involves wrapping the result of an operation in a custom class, typically called OperationResult or Result , which contains the result value and an indication of whether the operation was successful or not.
Benefits of the Operation Result Pattern
The Operation Result pattern offers several benefits, including:
- Improved Error Handling: By encapsulating the result of an operation, we can easily check if an operation was successful or not, and handle errors accordingly. This makes it easier to write robust and reliable code.
- Better Code Readability: The Operation Result pattern helps us write more readable code by separating the success and failure scenarios. This makes it easier for other developers to understand our code and maintain it over time.
- Reduced Code Duplication: By using the Operation Result pattern, we can avoid duplicating error handling code throughout our application, reducing code duplication and improving the overall maintainability of the codebase.
How to implement the Operation Result Pattern
The Operation result pattern is not a challenging pattern to implement and can be done so in very few lines. However, it can be a pattern that could be expanded upon to help cope with varying levels of complexity.
Let's take a look at implementing a minimum viable implementation of the pattern in C#.
We'll create a generic class called OperationResult<T>
, the name of the class is not all that important and it can be
named anything. I have just called it OperationResult to suit the context of the article. This class will encapsulate
the result of an operation along with an indication of whether the operation was successful.
Here is the breakdown of the logic contained within the class:
T
inOperationResult<T>
is a placeholder for a generic type. This will be substituted with a real type when you create instances ofOperationResult<T>
. For example, if you want to hold integer results, you would useOperationResult<int>
.public bool Success { get; }
: This is a property that indicates whether the operation was successful or not. As it only has a get;, it is read-only. The value can only be set from within the class, which is done via the constructor.public T Value { get; }
: This is a property of generic type T that holds the result of some operation. Again, it's read-only and can only be set in the constructor.public OperationResult(T value, bool success = true)
: This is the constructor for this class. It takes two parameters:- value of type
T
which represents the result of an operation. This can be any type, depending on how you instantiate theOperationResult<T>
class. - success of type bool, with a default value of true to indicate whether the operation was successful.
Within the constructor, it sets the property Success to the passed success value and Value to the passed value.
public readonly struct OperationResult<T>
{
public bool Success => !Errors.Any();
public T Value { get; }
public List<string> Errors { get; }
public OperationResult(T value, List<string> errors = null)
{
Value = value;
Errors = errors ?? new List<string>();
}
}
A simple example of how you could use this of this class would be as follows:
var operationResult = new OperationResult<string>("Hello, World!");
if (operationResult.Success)
{
Console.WriteLine(operationResult.Value);
}
In this example, "Hello, World!"
is the value that's been passed to the constructor, and Success is set to its default
value, true. The success of the operation is then evaluated, and if it was successful, the value is output to the console.
It may initially appear easier to rely on throwing exceptions when an operation fails. However, the Operation Result pattern is an alternative way of communicating success or failure between components when you don’t want to use exceptions. This pattern is useful when the messages are not errors, such as warning messages, or when treating an erroneous result is part of the main flow rather than a side catch flow.
A general rule of thumb, is that a method returning an operation result should not throw exceptions. This way, consumers don’t have to handle anything other than the operation result itself.
+---------------+
| Operation |
+---------------+
|
| (execute)
v
+-----------------------+ +---------------+
| OperationResult | | Success |
+-----------------------+ +---------------+
| -isSuccess: boolean | | -value: T |
| -getError(): Error | +---------------+
+-----------------------+ +---------------+
|
| (if not success)
v
+---------------+
| Errors |
+---------------+
|
| (handle error)
v
The schematic above illustrates the key elements of the Operation Result pattern
- Operation: The operation to be executed, which may succeed or fail.
- OperationResult: A class that encapsulates the result of the operation, including whether it was successful and any error that may have occurred.
- Success: A class that represents a successful operation result, which may include a value of type T.
- Errors: A Dictionary list that contains a list of errors that occurred during the operation.
The arrows in the diagram show the flow of execution:
- The operation is executed, and the result is wrapped in an OperationResult object.
- If the operation was successful, the OperationResult object contains a Success object with the result value.
- If the operation failed, the OperationResult object contains an Error object with information about the error.
- The caller can then handle the result of the operation by checking whether it was successful and, if not, handling the error.
Implementing different operation result patterns
In our Threenine.ApiResponse we implemented a little more complex implementation of the Operation Result Pattern to provide a result from operations in Web API's.
ApiResponse
A simple elegant library to handle returning responses from Mediator handlers back to REST API handlers implementing the Operation Result pattern.
threenine/ApiResponseIn the ApiResponse library we implement a Base response class which contains the basic error reporting features, that can be used by any class the inherits from it. The class is abstract, you cannot create instances of this class, but it can serve as a base class for other classes.
The BaseResponse
class provides a standardized way to return responses from methods, especially useful when you
want to encapsulate both the success state of an operation and its potential errors
protected BaseResponse(List<KeyValuePair<string, string[]>> errors = null)
declares a protected constructor. A
constructor with a protected modifier means it's only accessible within its own class and by derived classes. This
constructor includes a single optional parameter: a list of key-value pairs (KeyValuePair<string, string[]>)
.
Each key-value pair represents an error, where the key is the error message and the value is an array of strings.
Errors = errors ?? new List<KeyValuePair<string, string[]>>();
The ??
operator is called the null-coalescing operator.
If errors is null, a new empty list of KeyValuePair<string, string> is created and assigned to Errors. If errors is
not null, errors is assigned to Errors as is.
public bool IsValid => !Errors.Any();
This line is a read-only property that returns a bool. It uses the =>
operator
for a lambda expression, meaning IsValid will return true only if there are no errors (i.e., if !Errors.Any()
is true).
This is a convenient way to check whether the response instance contains any errors.
public List<KeyValuePair<string, string[]>> Errors { get; }
defines a public property Errors that is only gettable,
not settable from outside the class. It is of List<KeyValuePair<string, string[]>>
type, where each item is a
key-value pair representing an error. The 'key' is a string and the 'value' is an array of strings.
public abstract class BaseResponse
{
protected BaseResponse( List<KeyValuePair<string, string[]>> errors = null)
{
Errors = errors ?? new List<KeyValuePair<string, string[]>>();
}
public bool IsValid => !Errors.Any();
public List<KeyValuePair<string, string[]>> Errors { get; }
}
One of the result classes we implement making use of the BaseResponse
class is the SingleResponse
which also implements
the ISingleResponse
interface that works with a generic type TModel
. The where TModel : class
constraint ensures that
this generic type must be a reference type (a class).
The TModel
is a type parameter. The where keyword is used to define constraints on the kinds of types that the client
code can use for type arguments when it instantiates your class. where TModel : class
means the type argument must be
a reference type.
The constructor for the SingleResponse class. It takes two parameters:
TModel model
: An instance of the generic class TModel.List<KeyValuePair<string, string[]>> validationErrors = null
: An optional list of validation errors in the form of key-value pairs, where the key is a string and the value is a string array.
public TModel Item { get; }
sets the value of the Item property to the model parameter that was passed to the constructor.
The : base(validationErrors)
calls the base class constructor (BaseResponse) and passes the validationErrors parameter to it.
This class is used to wrap a single model instance and any associated errors into a common response format. It's common in APIs that need to provide a consistent response structure. Additionally, the Actions list may be used to provide metadata about what actions can be performed on the Item.
public class SingleResponse<TModel> : BaseResponse, ISingleResponse<TModel> where TModel : class
{
public SingleResponse(TModel model, List<KeyValuePair<string, string[]>> validationErrors = null) : base(validationErrors)
{
Item = model;
}
public TModel Item { get; }
}
Conclusion
The Operation Result pattern is a powerful tool for handling errors and exceptions in .NET applications. By encapsulating the result of an operation within a single object, developers can improve:
- Error handling
- Debugging
- Code organization
- Readability
Making use of the Operation Result pattern enables you to build more robust and maintainable applications that are better equipped to handle the challenges of modern software development.
Back-end software engineer
Experienced software developer, specialising in API Development, API Design API Strategy and Web Application Development. Helping companies thrive in the API economy by offering a range of consultancy services, training and mentoring.