-
Notifications
You must be signed in to change notification settings - Fork 16
Getting Started
Outcome is the ultimate result wrapper! It's great for those awkward moments when you need to return a value, but you might also need to return a list of validation messages, warnings, or a success bit.
Here's a simple example:
private IOutcome ValidateNumber(string number)
{
if (!number.IsNumeric())
{
return Outcomes.Failure()
.WithMessage("Invalid number (not numeric): " + number);
}
return Outcomes.Success();
}
Outcome really shines when you're orchestrating complex logic, because the fluent interface makes it easy to roll up messages from multiple methods, and even from unusual sources like exceptions.
Here's the classic scenario: you're calling a resource that works 99% of the time, but it's out on the internet somewhere, and could fail. You need wrap all your results in an object with metadata like:
- Did the request succeed?
- If it failed, why?
- The actual result needs to be in there too, if we actually have one.
So lets say, specifically, that we want to call a web service and then, if we get a valid response, do some important things with it. If it's a bad response, we need to handle that. In this example, we will just write the errors to the console and return, but in the real world you might do some logging and show a message to the user, set up a retry, etc. That part is beyond our scope.
Our code might look like this:
public void ProcessRequest()
{
IOutcome<ResultExample> ResultOutcome = ExecuteCallToWebService();
if (!ResultOutcome.Success)
{
Console.Write(ResultOutcome.Messages[0]); //Calling .ToMultiline() is better, it concats all messages and lets you specify a line break character.
return;
}
DoImportantThingsWithResult(ResultOutcome.Value);
}
As you can see, IOutcome is a generic wrapper that contains the result and some very basic metadata. The metadata includes:
- .Success: a Boolean indicating whether the request succeeded.
- .Messages[]: a List that usually contains failure messages.
- .Value: Not metadata strictly speaking, this is the data itself. It's of T, in this case ResultExample.
That's it. Nice and simple.
So how do we go about building these outcomes up inside the request?
Here's the code for ExecuteCallToWebService(), where the IOutcome is generated:
public IOutcome<ResultExample> ExecuteCallToWebService()
{
ResultExample Result;
try
{
Result = RiskyCall();
}
catch (Exception Ex)
{
return Outcomes.Failure<ResultExample>().FromException(Ex); //Adds Ex.Message to the message list
}
return Outcomes.Success<ResultExample>().WithValue(Result)
.WithMessage("It worked!");
}
So let's break down our two calls into Outcome.NET:
return Outcomes.Failure<ResultExample>().FromException(Ex);
Our call has failed and we need to return a failure state and a message. FromException() is a shortcut that grabs Exception.Message and puts it into IOutcome.Messages[] with a prefix of 'Exception: '. There are a lot of other ways we could have done this:
return Outcomes.Failure<ResultExample>().WithMessage("The stupid web service is down again!")
.FromException(Ex)
.WithMessage("Call IT!");
The API is very flexible, there's also a shortcut for just adding an exception with a prefix message:
return Outcomes.Failure<ResultExample>(Ex, "The stupid web service is down again!");
This works as well, although it doesn't give you any messaging:
return Outcomes.Failure<ResultExample>();
There's also a .FromOutcome() option that comes in handy in more complex scenarios, where you have a deep chain of method calls and they each add some detail.
The success API is very similar, but a bit simpler.
Here's our success outcome from the example above:
return Outcomes.Success<ResultExample>().WithValue(Result)
.WithMessage("It worked!");
- .WithValue(): Sets the value that we're wrapping.
- .WithMessage(): Adds a string to IOutcome.Messages[]. Usually you leave this empty for successes, but it can be useful in complicated scenarios.
There is also shorthand for just creating a success outcome with a value:
return Outcomes.Success<ResultExample>(Result);
And quite a few others, which you will find if you explore the API. They are functionally identical, it's just a matter of style.
I've written this code probably a hundred times over my career, and I'm sure across the world that number is more like hundreds of thousands. After working on a project that did a lot of integration, I decided to perfect my version of this wrapper and never write it again.
I'm sure you could slam together something that solves the same problem. You probably have in fact. It is fairly simple. But it might not be quite as expressive, it might not provide an easily recognizable pattern, and it definitely won't come with a dozen free unit tests.