C
C#9mo ago
Kiel

❔ Reducing Result-pattern annoyances

I know Result patterns can be a bit controversial as far as their benefits, but I'm giving them a try in my project. Anyways...I've outsourced some work to a scoped service which returns a Result<T>. My methods which call this outsourced code have this exact same structure:
var result = await service.DoSomethingAsync(...);
if (!result.IsSuccess)
return Response(result.ErrorMessage);

var obj = result.Value;
// do stuff with obj
var result = await service.DoSomethingAsync(...);
if (!result.IsSuccess)
return Response(result.ErrorMessage);

var obj = result.Value;
// do stuff with obj
Are there any ways to reduce this into a simpler form to further reduce the amount of code needed to perform the "try action, return error message if error else get success value" process? I certainly will concede I think I'm splitting hairs here and this is already simple enough for some, but I feel like I'm...missing something to make this more streamlined - maybe pattern matching could be utilized here?
5 Replies
Mle
Mle9mo ago
Hey Kiel, good question. Preferably you should provide the code that you have in the Result<T type that you have, but just from the info provided in your message, I might have some suggestions. You can define some commonly-used operations as higher order functions on the original result type. Don't be scared of the term higher order function, it's just a fancy way of saying a function that either accepts or returns a callback. (think System.Funcs, System.Actions of .NET).
public sealed class Result<T>
{
public string? ErrorMessage { get; }
public T? Value { get; }
public bool IsSuccess => Value is not null;
public bool IsError => !IsSuccess;
}
public sealed class Result<T>
{
public string? ErrorMessage { get; }
public T? Value { get; }
public bool IsSuccess => Value is not null;
public bool IsError => !IsSuccess;
}
I'm assuming you have something like this in your code, alongside some ctors or static factory methods to initalize instances of Result
pox
pox9mo ago
You can use something like the "TryParse" pattern if you want
Mle
Mle9mo ago
Let's start with some basic operations that you can try defining on this. Let's call the first one Map or you may know it better as Select (from Linq). It's purpose is to replace the value that can potentially be wrapped in the result or do nothing if the result is failed. This is more like pseudocode rather than something that will actually compile, but try to understand the concept.
public Result<TMapped> Map(Func<T, TMapped> mapper)
{
if (IsSuccess)
return Ok(mapper(Value));

return Error(ErrorMessage);
}
public Result<TMapped> Map(Func<T, TMapped> mapper)
{
if (IsSuccess)
return Ok(mapper(Value));

return Error(ErrorMessage);
}
And a usage of it would be similar to this.
async Task<Result<Transformed>> DoSomething()
{
Result<Original> firstStepResult = await service.TryFirstStepAsync();
return firstStepResult
.Map(v => {
return Transform(v);
});
}
async Task<Result<Transformed>> DoSomething()
{
Result<Original> firstStepResult = await service.TryFirstStepAsync();
return firstStepResult
.Map(v => {
return Transform(v);
});
}
Note that Transform will be called only if the first step was successful, otherwise the original error message would be propagated to the final result. This is a very simple case, in which only the first 'step' of the algorithm you're executing may fail. You can think of some other common cases and extract their logic to methods on the base Result type. For example. What if the second step may also fail and you don't want to endlessly wrap Results inside Results? What if you want the algorithm to continue even if one of the steps failed while aggregating all the errors in the process? What if some steps of the algorithm might need to execute asynchronously? What if you're not mapping the value wrapped in the result but instead doing something with it (like saving to a database?) What if you have a list of results and want to turn it into a Result of a list? Think about what's practical for you and what's not and try to extract the common logic to the Result class, it's not as anemic as it seems at first.
Kiel
Kiel9mo ago
Hey there, thanks for the thoughtful responses and sorry for not getting back sooner. ATM, my result class is as such:
public class Result<T>
where T : class
{
private Result(bool isSuccess)
{
IsSuccess = isSuccess;
}

public T? Value { get; private init; }

public string? ErrorMessage { get; private init; }

[MemberNotNullWhen(true, nameof(Value))]
[MemberNotNullWhen(false, nameof(ErrorMessage))]
public bool IsSuccess { get; }

public static Result<T> Success(T value) => new(true) { Value = value };
public static Result<T> Failure(string errorMessage) => new(false) { ErrorMessage = errorMessage };
public static Result<T> Failure(Exception exception) => Failure(exception.Message);

public static implicit operator Result<T>(T value) => Success(value);
public static implicit operator Result<T>(string errorMessage) => Failure(errorMessage);
}
public class Result<T>
where T : class
{
private Result(bool isSuccess)
{
IsSuccess = isSuccess;
}

public T? Value { get; private init; }

public string? ErrorMessage { get; private init; }

[MemberNotNullWhen(true, nameof(Value))]
[MemberNotNullWhen(false, nameof(ErrorMessage))]
public bool IsSuccess { get; }

public static Result<T> Success(T value) => new(true) { Value = value };
public static Result<T> Failure(string errorMessage) => new(false) { ErrorMessage = errorMessage };
public static Result<T> Failure(Exception exception) => Failure(exception.Message);

public static implicit operator Result<T>(T value) => Success(value);
public static implicit operator Result<T>(string errorMessage) => Failure(errorMessage);
}
ATM, the use case will likely not exceed the original design and usage that I proposed in the original message - I found this just a lot easier to use than Exception-based flow where I'd either return a value or throw an Exception - try/catch everywhere is ugly and also very repetitive it sucks I am limited by what C# is capable of as a language, as many SO results have pointed out, this is something functional languages are great at, and I have to just deal with the hand I'm dealt. In my perfect world, it would be cool to have a return statement on the right-hand side of a null coalescing operator like how it's possible with throw:
// Result.cs
public T? ValueOrDefault => IsSuccess ? Value : null;

// elsewhere...
var result = await service.DoSomethingAsync();
var obj = result.ValueOrDefault ?? return Response(result.ErrorMessage);

// do work with obj...
// Result.cs
public T? ValueOrDefault => IsSuccess ? Value : null;

// elsewhere...
var result = await service.DoSomethingAsync();
var obj = result.ValueOrDefault ?? return Response(result.ErrorMessage);

// do work with obj...
this would cut the boilerplate code needed to ensure obj was obtained successfully in half, which is a cool improvement. I wonder if there's a proposal out there...
Accord
Accord9mo ago
Was this issue resolved? If so, run /close - otherwise I will mark this as stale and this post will be archived until there is new activity.