Proposal for a New Result Type to Address Partial Failures in C#
Written on
Understanding the Need for a New Result Type
Previously, I shared my preferred implementation of a Result type in C#, which proved adequate for 95% of my requirements. However, I've recently encountered a scenario that necessitated a different approach.
If you haven’t yet read my earlier article on the initial Result type, I highly encourage you to do so, as this discussion frequently references it. You can view it here: Drawbacks of the Original Result Type.
In a recent project, I faced a situation where I needed to process multiple items simultaneously. While everything functioned smoothly 99% of the time, there were certain items—about 1%—that inherently failed due to specific properties.
It's essential for the task to conclude successfully despite these occasional failures. However, I wanted users to be notified of these minor exceptions so they could respond appropriately. The original Result type fell short in this regard, as returning a successful result would leave the caller unaware of potential issues, while a failed result could mislead them into thinking the entire operation was unsuccessful.
Architectural Considerations
I needed a way to inform users about mild errors without halting the function’s execution prematurely, as the impact of these errors was minimal. Several strategies could be employed to achieve this:
- Injecting a UserInformation Service: This would involve notifying users directly within the executing service where the errors occur.
- Returning a Result-Exception Tuple: This approach would convey mild exceptions when they arise.
- Creating a PartialResultType: This type would offer three distinct states: Success, Partial Success, and Error.
The first option would violate the single responsibility principle by incorporating user notification functionality into a method primarily designed for processing items. The second option, while functional, could still lead to confusion, as the caller would need to handle potential null values when no exception occurred. Therefore, I opted for the third solution: a Partial Result Type.
Introducing the Partial Result Type
Utilizing the foundational code from my previous Result type, I adapted it to accommodate partial results. For this purpose, I implemented an Enum labeled Success, which represents the values Success, PartialSuccess, and Error.
The term PartialSuccess aptly indicates that the underlying method executed successfully overall, but not completely. Unlike PartialFailure, which implies a complete failure, PartialSuccess allows for more nuanced handling by the caller.
Here’s my implementation for you to utilize:
public enum Success
{
Success,
PartialSuccess,
Error
}
public class PartialResult
{
protected readonly Exception? error;
public Success Success { get; protected set; }
public bool Succeeded => Success == Success.Success || Success == Success.PartialSuccess;
public bool SucceededPartially => Success == Success.PartialSuccess;
public string Message => error?.Message ?? "";
protected PartialResult(Success success, Exception? error)
{
Success = success;
this.error = error;
}
public Exception GetError() => error
?? throw new InvalidOperationException($"Error property for this Result not set.");
public static PartialResult Ok => new PartialResult(Success.Success, null);
public static PartialResult PartialOk(Exception error)
{
return new PartialResult(Success.PartialSuccess, error);}
public static PartialResult Error(Exception error)
{
return new PartialResult(Success.Error, error);}
public static implicit operator PartialResult(Exception exception) =>
new PartialResult(Success.Error, exception);
}
public sealed class PartialResult<TPayload> : PartialResult
where TPayload : class
{
private readonly TPayload? payload;
private PartialResult(TPayload? payload, Exception? error, Success success) : base(success, error)
{
this.payload = payload;}
public PartialResult(TPayload payload) : base(Success.Success, null)
{
this.payload = payload ?? throw new ArgumentNullException(nameof(payload));}
public PartialResult(TPayload payload, Exception error) : base(Success.PartialSuccess, error)
{
this.payload = payload ?? throw new ArgumentNullException(nameof(payload));}
public PartialResult(Exception error) : base(Success.Error, error)
{
}
public TPayload GetOk() => Success == Success.Success || Success == Success.PartialSuccess
? payload ?? throw new InvalidOperationException($"Payload for Result<{typeof(TPayload)}> was not set.") : throw new InvalidOperationException($"Operation for Result<{typeof(TPayload)}> was not successful.");
public new Exception GetError() => error
?? throw new InvalidOperationException($"Error property for Result<{typeof(TPayload)}> not set.");
public new static PartialResult<TPayload> Ok(TPayload payload)
{
return new PartialResult<TPayload>(payload, null, Success.Success);}
public static PartialResult<TPayload> PartialOk(TPayload payload, Exception error)
{
return new PartialResult<TPayload>(payload, error, Success.PartialSuccess);}
public new static PartialResult<TPayload> Error(Exception error)
{
return new PartialResult<TPayload>(null, error, Success.Error);}
public static implicit operator PartialResult<TPayload>(TPayload payload) =>
new(payload, null, Success.Success);
public static implicit operator PartialResult<TPayload>(Exception exception) =>
new(exception);
}
As illustrated, we now have a PartialOk initializer that yields a successful result while still flagging an exception for the caller to check.
When to Implement Partial Results
While the Partial Result type is a powerful tool, it should be employed judiciously. When a caller receives a Partial Result, they must always verify if there was a partial success, which may not be necessary in many cases.
It’s advisable to implement both Result and PartialResult within your codebase and apply each where appropriate.
If you haven’t explored the original proposed Result type, I encourage you to do so! It not only outlines its functionality but also shares various useful extensions I’ve conceived while working with it.
Furthermore, if you wish to examine the source code for the PartialResult type, please visit my GitHub Repository.
Thank you for taking the time to read this piece. I hope you found it insightful and enriching. Your support and engagement mean a lot to me.
If you’re interested in staying informed about the latest trends, tips, and techniques in clean architecture, clean coding, and modern tech stacks—particularly with C#, .NET, and Angular—I would be grateful if you considered following me.
Wishing you a fantastic day! By doing so, you'll gain access to an invaluable platform that connects new writers and readers while fostering daily learning opportunities.