Chapter 11: Understanding the Operation Result Design Pattern

In this chapter, we explore the Operation Result pattern starting from simple to more complex cases. An operation result aims at communicating the success or the failure of an operation to its caller. It also allows that operation to return both a value and one or more messages to the caller.

Imagine any system in which you want to display user-friendly error messages, achieve some small speed gain, or even handle failure easily and explicitly. The Operation Result design pattern can help you achieve these goals. One way to use it is as the result of a remote operation, such as after querying a remote web service.

The following topics will be covered in this chapter:

  • The Operation Result design pattern basics
  • The Operation Result design pattern returning a value
  • The Operation Result design pattern returning error messages
  • The Operation Result design pattern returning messages with severity levels
  • Using sub-classes and static factory methods for better isolation of successes and failures

Goal

The role of the Operation Result pattern is to give an operation (a method) the possibility to return a complex result (an object), which allows the consumer to:

  • [Mandatory] Access the success indicator of the operation (that is, whether the operation succeeded or not).
  • [Optional] Access the result of the operation, in case there is one (the return value of the method).
  • [Optional] Access the cause of the failure, in case the operation was not successful (error messages).
  • [Optional] Access other information that documents the operation's result. This could be as simple as a list of messages or as complex as multiple properties.

This can go even further, such as returning the severity of a failure or adding any other relevant information for the specific use case. The success indicator could be binary (true or false), or there could be more than two states, such as success, partial success, and failure. Your imagination (and your needs) is your limit!

Tip

Focus on your needs first, then use your imagination to reach the best possible solution. Software engineering is not only about applying techniques that others told you to. It's an art! The only difference is that you are crafting software instead of painting or woodworking. And that most people won't see any of that art (code).

Design

It is easy 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, or can't, use exceptions.

To be used effectively, a method must return an object containing one or more elements presented in the Goal section. As a rule of thumb, a method returning an operation result should not throw an exception. This way, consumers don't have to handle anything else other than the operation result itself. For special cases, you could allow exceptions to be thrown, but at that point, it would be a judgment call based on clear specifications or facing a real problem.

Instead of walking you through all of the possible UML diagrams, let's jump into the code and explore multiple smaller examples after taking a look at the basic sequence diagram that describes the simplest form of this pattern, applicable to all examples:

Figure 11.1 – Sequence diagram of the Operation Result design pattern

As we can see from the diagram, an operation returns a result (an object), and then the caller can handle that result. What can be included in that result object is covered in the following examples.

Project – Implementing different Operation Result patterns

In this project, a consumer routes the HTTP requests to the right handler. We are visiting each of those handlers one by one, which will help us implement simple to more complex operation results. This should show you the many alternative ways to implement the Operation Result pattern to help you understand it, make it your own, and implement it as required in your projects.

The consumer

The consumer in all examples is the Startup class. The following code routes the request toward a handler:

app.UseRouter(builder =>

{

    builder.MapGet("/simplest-form", SimplestFormHandler);

    builder.MapGet("/single-error", SingleErrorHandler);

    builder.MapGet("/single-error-with-value", SingleErrorWithValueHandler);

    builder.MapGet("/multiple-errors-with-value", MultipleErrorsWithValueHandler);

    builder.MapGet("/multiple-errors-with-value-and-severity", MultipleErrorsWithValueAndSeverityHandler);

    builder.MapGet("/static-factory-methods", StaticFactoryMethodHandler);

});

Next, we cover each handler one by one.

Its simplest form

The following diagram represents the simplest form of the Operation Result pattern:

Figure 11.2 – Class diagram of the Operation Result design pattern

Figure 11.2 – Class diagram of the Operation Result design pattern

We can translate that class diagram into the following blocks of code:

namespace OperationResult

{

    public class Startup

    {

        // ...

        private async Task SimplestFormHandler(HttpRequest request, HttpResponse response, RouteData data)

        {

            // Create an instance of the class that contains the operation

            var executor = new SimplestForm.Executor();

            // Execute the operation and handle its result

            var result = executor.Operation();

            if (result.Succeeded)

            {

                // Handle the success

                await response.WriteAsync("Operation succeeded");

            }

            else

            {

                // Handle the failure

                await response.WriteAsync("Operation failed");

            }

        }

    }

}

The preceding code handles the /simplest-form HTTP request. It is the consumer of the operation.

namespace OperationResult.SimplestForm

{

    public class Executor

    {

        public OperationResult Operation()

        {

            // Randomize the success indicator

            // This should be real logic

            var randomNumber = new Random().Next(100);

            var success = randomNumber % 2 == 0;

            // Return the operation result

            return new OperationResult(success);

        }

    }

    public class OperationResult

    {

        public OperationResult(bool succeeded)

        {

            Succeeded = succeeded;

        }

        public bool Succeeded { get; }

    }

}

The Executor class implements the operation to execute with the Operation method. That method returns an instance of the OperationResult class. The implementation is based on a random number. Sometimes it succeeds, and sometimes it fails. You would usually code some application logic in that method instead.

The OperationResult class represents the result of the operation. In this case, it is a simple read-only boolean value stored in the Succeeded property.

In this form, the difference between the Operation() method returning a bool and an instance of OperationResult is thin, but it exists nonetheless. By returning an OperationResult object, you can extend the return value over time, adding to it, which you cannot do with a bool without updating all consumers.

A single error message

Now that we know whether the operation succeeded or not, we want to know what went wrong. To do that, we can add an ErrorMessage property to the OperationResult class. By doing that, we no longer need to set whether the operation succeeded or not; we could compute that using the ErrorMessage property instead.

The logic behind this improvement is as follows:

  • When there is no error message, the operation succeeded.
  • When there is an error message, the operation failed.

The OperationResult implementing this logic looks like the following:

namespace OperationResult.SingleError

{

    public class OperationResult

    {

        public OperationResult() { }

        public OperationResult(string errorMessage)

        {

            ErrorMessage = errorMessage ?? throw new ArgumentNullException(nameof(errorMessage));

        }

        public bool Succeeded => string.IsNullOrWhiteSpace(ErrorMessage);

        public string ErrorMessage { get; }

    }

}

In the preceding code, we have the following:

  • Two constructors:

    a) The parameterless constructor that handles successes.

    b) The constructor that takes an error message as a parameter that handles errors.

  • The Succeeded property that checks for an ErrorMessage.
  • The ErrorMessage property that contains an optional error message.

The executor of that operation looks similar but uses the new constructors, setting an error message instead of directly setting the success indicator:

namespace OperationResult.SingleError

{

    public class Executor

    {

        public OperationResult Operation()

        {

            // Randomize the success indicator

            // This should be real logic

            var randomNumber = new Random().Next(100);

            var success = randomNumber % 2 == 0;

            // Return the operation result

            return success

                ? new OperationResult()

                : new OperationResult($"Something went wrong with the number '{randomNumber}'.");

        }

    }

}

The consuming code does the same as in the previous sample but writes the error message in the response output instead of a generic failure string:

namespace OperationResult

{

    public class Startup

    {

        // ...

        private async Task SingleErrorHandler(HttpRequest request, HttpResponse response, RouteData data)

        {

            // Create an instance of the class that contains the operation

            var executor = new SingleError.Executor();

            // Execute the operation and handle its result

            var result = executor.Operation();

            if (result.Succeeded)

            {

                // Handle the success

                await response.WriteAsync("Operation succeeded");

            }

            else

            {

                // Handle the failure

                await response.WriteAsync (result.ErrorMessage);

            }

        }

    }

}

When looking at that example, we can begin to comprehend the Operation Result pattern's usefulness. It gets us further away from the simple success indicator that looked like an overcomplicated boolean. And this is not the end of our exploration because many more forms can be designed and used in more complex scenarios.

Adding a return value

Now that we have a reason for failure, we may want the operation to return a value. To achieve this, let's start from the last example and add a Value property to the OperationResult class as follows (we cover init-only properties in Chapter 17, ASP.NET Core User Interfaces):

namespace OperationResult.SingleErrorWithValue

{

    public class OperationResult

    {

        // ...

        public int? Value { get; init; }

    }

}

The operation is also very similar, but we are setting the Value using the object initializer:

namespace OperationResult.SingleErrorWithValue

{

    public class Executor

    {

        public OperationResult Operation()

        {

            // Randomize the success indicator

            // This should be real logic

            var randomNumber = new Random().Next(100);

            var success = randomNumber % 2 == 0;meet

            // Return the operation result

            return success

                ? new OperationResult { Value =

                randomNumber }

                : new OperationResult($"Something went wrong with the number '{randomNumber}'.")

                {

                    Value = randomNumber

                };

        }

    }

}

With that in place, the consumer can use the Value as follows:

namespace OperationResult

{

    public class Startup

    {

        // ...

        private async Task SingleErrorWithValueHandler (HttpRequest request, HttpResponse response, RouteData data)

        {

            // Create an instance of the class that contains the operation

            var executor = new SingleErrorWithValue.Executor();

            // Execute the operation and handle its result

            var result = executor.Operation();

            if (result.Succeeded)

            {

                // Handle the success

                await response.WriteAsync($"Operation succeeded with a value of '{result.Value} '.");

            }

            else

            {

                // Handle the failure

                await response.WriteAsync (result.ErrorMessage);

            }

        }

    }

}

As we can see from this sample, we can display a relevant error message when the operation fails and use a return value when it succeeds (or even when it fails in this case), and all of that without throwing an exception. With this, the power of the Operation Result pattern starts to emerge. We are not done yet, so let's jump into the next evolution.

Multiple error messages

Now we are at the point where we can transfer a Value and an ErrorMessage to the operation consumers, but what about transferring multiple errors, such as validation errors? To achieve this, we can convert our ErrorMessage property into an IEnumerable<string> and add methods to manage messages:

namespace OperationResult.MultipleErrorsWithValue

{

    public class OperationResult

    {

        private readonly List<string> _errors;

        public OperationResult(params string[] errors)

        {

            _errors = new List<string>(errors ?? Enumerable.Empty<string>());

        }

        public bool Succeeded => !HasErrors();

        public int? Value { get; set; }

        public IEnumerable<string> Errors => new ReadOnlyCollection<string>(_errors);

        public bool HasErrors()

        {

            return Errors?.Count() > 0;

        }

        public void AddError(string message)

        {

            _errors.Add(message);

        }

    }

}

Let's take a look at the new pieces in the preceding code before continuing:

  • The errors are now stored in List<string> _errors and returned to the consumers through a ReadOnlyCollection<string> instance that is hidden under an IEnumerable<string> interface. The ReadOnlyCollection<string> instance denies the ability to change the collection from the outside, assuming the consumer is clever enough to cast IEnumerable<string> to List<string>, for example.
  • The Succeeded property was updated to account for a collection instead of a single message and follows the same logic.
  • The HasErrors method was added for convenience.
  • The AddError method allows adding errors after the instance creation, which could happen in more complex scenarios, such as multi-step operations where parts could fail without the operation itself failing.

    Note

    For additional control over the result, the AddError method should be hidden from the outside of the operation. That way, a consumer could not add errors to the result after the completion of the operation. Of course, the level of control that is required depends on each specific scenario. A way to code this would be to return an interface that does not contain the method, instead of the concrete type that does.

Now that the operation result is updated, the operation itself can stay the same, but we could add more than one error to the result. The consumer must handle that slight difference and support multiple errors.

Let's take a look at that code:

namespace OperationResult

{

    public class Startup

    {

        private async Task MultipleErrorsWithValueHandler(HttpRequest request, HttpResponse response, RouteData data)

        {

            // Create an instance of the class that contains the operation

            var executor = new MultipleErrorsWithValue.Executor();

            // Execute the operation and handle its result

            var result = executor.Operation();

            if (result.Succeeded)

            {

                // Handle the success

                await response.WriteAsync($"Operation succeeded with a value of '{result.Value}'.");

            }

            else

            {

                // Handle the failure

                var json = JsonSerializer.Serialize (result.Errors);

                response.Headers["ContentType"] = "application/json";

                await response.WriteAsync(json);

            }

        }

    }

}

The code serializes the IEnumerable<string> Errors property to JSON before outputting it to the client to help visualize the collection.

Tip

Returning a plain/text string when the operation succeeds and an application/json array when it fails is usually not a good idea. I suggest not doing something similar to this in a real application. Either return JSON or plain text. Try not to mix content types in a single endpoint. In most cases, mixing content types will only create avoidable complexity. We could say that reading content-type and the status code headers would be enough to know what has been returned by the server, and that's the purpose of those headers in the HTTP specifications. But, even if that is true, it is way easier for your fellow developers to always be able to expect the same content type when consuming your APIs.

When designing systems' contracts, consistency and uniformity are usually better than incoherency, ambiguity, and variance.

Our Operation Result pattern implementation is getting better and better but still lacks a few features. One of those features is the possibility of propagating messages that are not errors, such as information messages and warnings, which we will implement next.

Adding message severity

Now that our operation result structure is materializing, let's update our last iteration to support message severity.

First, we need a severity indicator. An enum is a good candidate for this kind of work, but it could be something else. Let's name it OperationResultSeverity.

Then we need a message class to encapsulate both the message and the severity level; let's name that class OperationResultMessage. The new code looks like this:

namespace OperationResult.WithSeverity

{

    public class OperationResultMessage

    {

        public OperationResultMessage(string message, OperationResultSeverity severity)

        {

            Message = message ?? throw new ArgumentNullException(nameof(message));

            Severity = severity;

        }

        public string Message { get; }

        public OperationResultSeverity Severity { get; }

    }

    public enum OperationResultSeverity

    {

        Information = 0,

        Warning = 1,

        Error = 2

    }

}

As you can see, we have a simple data structure to replace our string messages.

Then we need to update the OperationResult class to use that new OperationResultMessage class. We need to ensure that the operation result indicates a success only when there is no OperationResultSeverity.Error, allowing it to transmit the OperationResultSeverity.Information and OperationResultSeverity.Warnings messages:

namespace OperationResult.WithSeverity

{

    public class OperationResult

    {

        private readonly List<OperationResultMessage> _messages;

        public OperationResult(params OperationResultMessage[] errors)

        {

            _messages = new List<OperationResultMessage> (errors ?? Enumerable.Empty <OperationResultMessage>());

        }

        public bool Succeeded => !HasErrors();

        public int? Value { get; init; }

        public IEnumerable<OperationResultMessage> Messages

            => new ReadOnlyCollection <OperationResultMessage>(_messages);

        public bool HasErrors()

        {

            return FindErrors().Count() > 0;

        }

        public void AddMessage(OperationResultMessage message)

        {

            _messages.Add(message);

        }

        private IEnumerable<OperationResultMessage> FindErrors()

            => _messages.Where(x => x.Severity == OperationResultSeverity.Error);

    }

}

The highlighted lines represent the new logic setting the success state only when no error is present in the _messages list.

With that in place, the Executor class also needs to be revamped.

So let's look at that new version of the Executor class:

namespace OperationResult.WithSeverity

{

    public class Executor

    {

        public OperationResult Operation()

        {

            // Randomize the success indicator

            // This should be real logic

            var randomNumber = new Random().Next(100);

            var success = randomNumber % 2 == 0;

            // Some information message

            var information = new OperationResultMessage(

                "This should be very informative!",

                OperationResultSeverity.Information

            );

            // Return the operation result

            if (success)

            {

                var warning = new OperationResultMessage(

                    "Something went wrong, but we will try again later automatically until it works!",

                    OperationResultSeverity.Warning

                );

                return new OperationResult(information, warning) { Value = randomNumber };

            }

            else

            {

                var error = new OperationResultMessage(

                    $"Something went wrong with the number '{randomNumber}'.",

                    OperationResultSeverity.Error

                );

                return new OperationResult(information, error) { Value = randomNumber };

            }

        }

    }

}

As you may have noticed, we removed the tertiary operator to make the code easier to read.

Tip

You should always aim to write code that is easy to read. It is OK to use the language features, but nesting statements over statements on a single line has its limits and can quickly become a mess.

In that last code block, both successes and failures return two messages:

  • When it is successful, the messages are an information message and a warning.
  • When it is unsuccessful, the messages are an information message and an error.

From the consumer standpoint (see the following code), we now only serialize the result to the output to clearly show the outcome. Here is the /multiple-errors-with-value-and-severity endpoint delegate that consumes this new operation:

namespace OperationResult

{

    public class Startup

    {

        // ...

        private async Task MultipleErrorsWithValueAndSeverityHandler(HttpRequest request, HttpResponse response, RouteData data)

        {

            // Create an instance of the class that contains the operation

            var executor = new WithSeverity.Executor();

            // Execute the operation and handle its result

            var result = executor.Operation();

            if (result.Succeeded)

            {

                // Handle the success

            }

            else

            {

                // Handle the failure

            }

            var json = JsonSerializer.Serialize(result);

            response.Headers["ContentType"] = "application/json";

            await response.WriteAsync(json);

        }

    }

}

As you can see, it is still as easy to use, but now with more flexibility added to it. We could do something with the different types of messages, such as displaying them to the user, retrying the operation, and more.

For now, if you run the application and call that endpoint, successful calls should return a JSON string that looks like the following:

{

    "Succeeded": true,

    "Value": 86,

    "Messages": [

        {

            "Message": "This should be very informative!",

            "Severity": 0

        },

        {

            "Message": "Something went wrong, but we will try again later automatically until it works!",

            "Severity": 1

        }

    ]

}

Failures should return a JSON string that looks like this:

{

    "Succeeded": false,

    "Value": 87,

    "Messages": [

        {

            "Message": "This should be very informative!",

            "Severity": 0

        },

        {

            "Message": "Something went wrong with the

             number '87'.",

            "Severity": 2

        }

    ]

}

One more idea to improve this design would be to add a Status property that returns a complex success result based on each message's severity level. To do that, we could create another enum:

public enum OperationStatus{ Success, Failure, PartialSuccess}

Then we could access that through a new property named Status, on the OperationResult class. With this, a consumer could handle partial successes without digging into the messages themselves. I will leave you to play with this one on your own.

Now that we've expanded our simple example into this, what happens if we want the Value to be optional? To do that, we could create multiple operation result classes that each hold more or less information (properties); let's try that next.

Sub-classes and factories

In this iteration, we keep all of the properties, but we change how we instantiate the OperationResult objects.

A static factory method is nothing more than a static method with the responsibility to create objects. As you are about to see, it can become handy for ease of use. As always, I cannot stress this enough: be careful when designing something static, or it could haunt you later.

Let's start with some already-visited code:

namespace OperationResult.StaticFactoryMethod

{

    public class OperationResultMessage

    {

        public OperationResultMessage(string message, OperationResultSeverity severity)

        {

            Message = message ?? throw new ArgumentNullException(nameof(message));

            Severity = severity;

        }

        public string Message { get; }

        public OperationResultSeverity Severity { get; }

    }

    public enum OperationResultSeverity

    {

        Information = 0,

        Warning = 1,

        Error = 2

    }

}

The preceding code is the same as we used before. In the following block of code, the severity is not considered when computing the operation's success or failure result. Instead, we create an abstract OperationResult class with two sub-classes:

  • SuccessfulOperationResult, which represents successful operations.
  • FailedOperationResult, which represents failed operations.

Then the next step is to force the use of the specifically designed class by creating two static factory methods:

  • public static OperationResult Success(), which returns a SuccessfulOperationResult.
  • public static OperationResult Failure(params OperationResultMessage[] errors), which returns a FailedOperationResult.

Doing this moves the responsibility of deciding whether the operation is a success or not from the OperationResult class itself to the Operation method.

The following code block shows the new OperationResult implementation (the static factory is highlighted):

namespace OperationResult.StaticFactoryMethod

{

    public abstract class OperationResult

    {

        private OperationResult() { }

        public abstract bool Succeeded { get; }

        public virtual int? Value { get; init; }

        public abstract IEnumerable<OperationResultMessage> Messages { get; }

        public static OperationResult Success(int? value = null)

        {

            return new SuccessfulOperationResult { Value = value };

        }

        public static OperationResult Failure(params OperationResultMessage[] errors)

        {

            return new FailedOperationResult(errors);

        }

        public sealed class SuccessfulOperationResult : OperationResult

        {

            public override bool Succeeded => true;

            public override IEnumerable <OperationResultMessage> Messages

                => Enumerable.Empty <OperationResultMessage>();

        }

        public sealed class FailedOperationResult : OperationResult

        {

            private readonly List<OperationResultMessage> _messages;

            public FailedOperationResult(params OperationResultMessage[] errors)

            {

                _messages = new List<OperationResultMessage>(errors ?? Enumerable.Empty<OperationResultMessage>());

            }

            public override bool Succeeded => false;

            public override IEnumerable <OperationResultMessage> Messages

                => new ReadOnlyCollection <OperationResultMessage>(_messages);

        }

    }

}

After analyzing the code, there are two closely related particularities:

  • The OperationResult class has a private constructor.
  • Both the SuccessfulOperationResult and FailedOperationResult classes are nested inside OperationResult and inherit from it.

Nested classes are the only way to inherit from the OperationResult class because, as a member of the class, nested classes have access to its private members, including the constructor. Otherwise, it is impossible to inherit from OperationResult.

Since the beginning of the book, I have repeated flexibility many times; but you don't always want flexibility. Sometimes you want control over what you expose and what you allow consumers to do.

In this specific case, we could have used a protected constructor instead, or we could have implemented a fancier way of instancing successes and failures instances. However, I decided to use this opportunity to show you how to lock an implementation in place, making it impossible to extend by inheritance from the outside. We could have built mechanisms in our classes to allow controlled extensibility, but for this one, let's keep it locked in tight!

From there, the only missing pieces are the operation itself and the client that consumes that operation. Let's take a look at the operation first:

namespace OperationResult.StaticFactoryMethod

{

    public class Executor

    {

        public OperationResult Operation()

        {

            // Randomize the success indicator

            // This should be real logic

            var randomNumber = new Random().Next(100);

            var success = randomNumber % 2 == 0;

            // Return the operation result

            if (success)

            {

                return OperationResult.Success(randomNumber);

            }

            else

            {

                var error = new OperationResultMessage(

                    $"Something went wrong with the number '{randomNumber}'.",

                    OperationResultSeverity.Error

                );

                return OperationResult.Failure(error);

            }

        }

    }

}

The two highlighted lines in the preceding code block show the elegance of this new improvement. I find this code very easy to read, which was the objective. We now have two methods that clearly define our intentions when using them: Success or Failure.

The consumer uses the same code that we saw before in other examples, so I'll omit it here.

Advantages and disadvantages

Here are a few advantages and disadvantages that come with the Operation Result design pattern.

Advantages

It is more explicit than throwing an Exception since the operation result type is specified explicitly as the method's return type. That makes it more evident than knowing what type of exceptions the operation and its dependencies can throw.

Another advantage is the execution speed; returning an object is faster than throwing an exception. Not that much faster, but faster nonetheless.

Disadvantages

Using operation results is more complex than throwing exceptions because we must manually propagate it up the call stack (a.k.a. returned by the callee and handled by the caller). This is especially true if the operation result must go up multiple levels, which could be an indicator not to use the pattern.

It is easy to expose members that are not used in all scenarios, creating an API surface that is bigger than needed, where some parts are used only in some cases. But, between this and spending countless hours designing the perfect system, sometimes exposing an int? Value { get; } property can be a more than viable option. From there, you have lots of options to reduce that surface to a minimum. Use your imagination and your designing skills to overcome those challenges!

Summary

In this chapter, we visited multiple forms of the Operation Result pattern, from an augmented boolean to a complex data structure containing messages, values, and success indicators. We also explored static factories and private constructors to control external access. Furthermore, after all of that exploring, we can conclude that there are almost endless possibilities around the Operation Result pattern. Each specific use case should dictate how to make it happen. From here, I am confident that you have enough information about the pattern to explore the many more possibilities by yourself, and I highly encourage you to.

At this point, we would usually explore how the Operation Result pattern can help us follow the SOLID principles. However, it depends too much on the implementation, so here are a few points instead:

  • The OperationResult class encapsulates the result, extracting that responsibility from the other system's components (SRP).
  • We violated the ISP with the Value property in multiple examples. This was minor and could have been done differently, which could lead to a more complex design.
  • We could compare an operation result to a view model or a DTO, but returned by an operation (method). From there, we could add an abstraction or stick with returning a concrete class, which we could see as a violation of the DIP.
  • When the advantages surpass the minor and limited impacts of those two violations, I don't mind letting them slide (principles are ideals, rules, not laws).

This chapter concludes the Designing at Component Scale section and leads to the Designing at Application Scale section, in which we'll explore higher-level design patterns.

Questions

Let's take a look at a few practice questions:

  1. Is returning an operation result when doing an asynchronous call, such as an HTTP request, a good idea?
  2. What is the name of the pattern that we implemented using static methods?
  3. Is it faster to return an operation result than throwing an exception?

Further reading

Here are some links to build on what we learned in this chapter:

  • An article on my blog about exceptions (title: A beginner guide to exceptions | The basics): https://net5.link/PpEm
  • An article on my blog about Operation Result (title: Operation result | Design Pattern): https://net5.link/4o2q
  • I have a generic open source implementation of the Operation Result pattern, allowing you to use it by adding a NuGet package to your project: https://net5.link/FeGZ
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.188.40.207