Skip to content

Conversation

@nandos13
Copy link
Contributor

Description

Successful results and problematic results should be mutually exclusive. Failed results should always have a problem by design, and successful results should not. This PR ensures Problem can never be null when IsSuccess is false. Additionally, it cleans up cases where a result could be both successful and failed.

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update
  • Performance improvement
  • Code refactoring

Changes Made

  • Changes mentioned here were applied to Result, ResultandCollectionResulttypes. I'll just refer toResult` for simplicity.
  • Changed Result constructor from internal to private.
  • Replaced internal Create methods with a CreateSuccess and CreateFailed method to help enforce parameters in either case.
  • All creation methods (ie. Result.Fail(...)) now use these new methods, whereas previously some cases directly used the constructor.
  • (Breaking) Changed IsSuccess set method from init to private init.
  • (Breaking) Changed Problem set method from set to private init.
  • (Breaking) Removed IResultProblem.Problem.set property setter.
  • Implemented a backing field for the Problem property. Problem.get checks if the backing field is null and success is false, and assigns a generic error in this case. This will only occur if the user uses the default result value, ie. Result r = default;. Also note that the Json-related attributes on Problem property have been moved to the backing field, and the property now has a [JsonIgnore] attribute.
  • Added [JsonInclude] attributes to new private data to preserve deserialization.
  • Changed behavior of AddInvalidMessage methods to throw an InvalidOperationException when invoked on a successful result.
  • Fixed several tests that asserted HasProblem.Should().BeFalse() for failed results. These tests used Fail(...) overloads that previously did not assign a Problem, hence the assertion. These tests now assert Should().BeTrue().

Testing

  • Unit tests pass
  • Integration tests pass
  • Manual testing completed

Checklist

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • Any dependent changes have been merged and published

Related Issues

Closes #32
Closes #35

Screenshots (if applicable)

Additional Notes

Since IsSuccess & Problem properties can no longer be publicly set, users can no longer construct a result like so: new Result { IsSuccess = false, Problem = ... };.

managedcode#32
managedcode#35

Begin work to improve Result state safety, ie disallow a Result to be both successful & failed, and to ensure failed results are never missing a Problem.
Made constructors for Result types private.
Replaced Result Create methods with CreateSuccess & CreateFailed to force parameters for either case.
Result.Problem will never be null when the result is unsuccessful. If no problem was recorded, it falls back to returning Problem.GenericError.
Repeated changes from last commit in the CollectionResult type.
IsFailed and HasProblem properties just return the opposite of IsSuccess, since failed results now always have a Problem by design.
ThrowIfFail implementations don't rely on HasProblem. HasProblem is now functionally equivalent to IsFailed and is only necessary for the IResultProblem interface.
Added [JsonInclude] attribute to IsSuccess properties, as init setter is now private.
Moved Json-related attributes from Problem properties to _problem backing fields.
Added [JsonIgnore] attribute to Problem properties, as the backing field should now be serialized.
Added [JsonInclude] attribute to _problem fields.
Changed several tests that expect 'HasProblem' to be false in the case of Fail methods that previously did not assign a Problem.
This change has been made under the assumption that this is indeed an oversight: managedcode#32 (comment)
Change tests that use the AddInvalidMessage methods to operate on a failed result, since said methods now throw when invoked on a successful result.
Restore a private unused constructor for Result<T>, which is accessed via Activator.CreateInstance in CommunicationOutgoingGrainCallFilter
Added two new tests that assert the AddInvalidMessage methods should throw an InvalidOperationException when the result is successful
@codecov
Copy link

codecov bot commented Aug 20, 2025

Codecov Report

❌ Patch coverage is 86.79245% with 21 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (main@9e2a531). Learn more about missing BASE report.

Files with missing lines Patch % Lines
ManagedCode.Communication/Result/Result.cs 72.72% 6 Missing ⚠️
ManagedCode.Communication/ResultT/Result.cs 78.26% 5 Missing ⚠️
...ommunication/CollectionResultT/CollectionResult.cs 81.81% 4 Missing ⚠️
...cation/CollectionResultT/CollectionResultT.Fail.cs 90.90% 2 Missing ⚠️
ManagedCode.Communication/Result/Result.Fail.cs 90.00% 2 Missing ⚠️
ManagedCode.Communication/ResultT/ResultT.Fail.cs 90.47% 2 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main      #40   +/-   ##
=======================================
  Coverage        ?   75.24%           
=======================================
  Files           ?       75           
  Lines           ?     1850           
  Branches        ?      292           
=======================================
  Hits            ?     1392           
  Misses          ?      458           
  Partials        ?        0           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR enforces mutual exclusivity between successful and failed results by ensuring failed results always have a Problem and successful results never do. The changes prevent inconsistent state where a result could be both successful and have a problem.

  • Replaced Create method with separate CreateSuccess and CreateFailed methods to enforce proper parameters
  • Made IsSuccess and Problem properties immutable to prevent external modification
  • Added automatic fallback to generic error for failed results without an explicit problem
  • Updated all fail methods to use the new creation pattern and ensure Problem is always assigned

Reviewed Changes

Copilot reviewed 23 out of 23 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
ManagedCode.Communication/ResultT/Result.cs Core Result implementation with new private constructor and separate creation methods
ManagedCode.Communication/Result/Result.cs Core Result implementation with new private constructor and separate creation methods
ManagedCode.Communication/CollectionResultT/CollectionResult.cs Core CollectionResult implementation with new private constructor and separate creation methods
ManagedCode.Communication/IResultProblem.cs Interface change removing Problem setter
Various Fail/Succeed method files Updated to use new CreateSuccess/CreateFailed methods
Various test files Updated assertions to reflect that failed results now always have problems
Orleans converter files Updated to use new creation methods in surrogate converters
AspNetCore extension files Updated to use GetProblemNoFallback() to avoid automatic generic error fallback

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Problem = Problem.Validation((key, value));
}
else
if (IsSuccess) throw new InvalidOperationException("Cannot add invalid message to a successful result");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe here we can change success to failed? in this case?
just asking

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this would be a better solution. Please let me know what you think and if you'd like me to make any changes and resubmit.

The AddInvalidMessage code is already duplicated between Result, Result<T> and CollectionResult<T>. We could instead move it to the Problem class to reduce duplication. The Result.AddInvalidMessage methods would be best to become obsolete:

// Make the Result.AddInvalidMessage methods obsolete
[Obsolete("Use Problem.AddInvalidMessage instead")]
public void AddInvalidMessage(string message)
{
    if (!IsSuccess)
        Problem.AddInvalidMessage(message);

    // else, do nothing
}

And here is a usage example:

if (result.IsSuccess)
{
    // It does not make sense to add invalid messages to the successful result.
    // If we really need to do that, make a new one.
    var invalidResult = Result.Invalid("message");
}
else
{
    result.Problem.AddInvalidMessage("message");
}

// User can still call Result.AddInvalidMessage without checking IsSuccess,
// it just won't do anything if successful; Result will remain successful.
result.AddInvalidMessage("message");

Benefits:

  1. We retain state immutability of Results.
  2. We reduce code duplication.
  3. Readability improves. It is clear to the user that they should only add invalid messages to an existing Problem, not to a successful result.
  4. The throw is removed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've implemented the idea I mentioned above. Please let me know your thoughts, thanks

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I love it!

Moved main implementation of AddInvalidMessage methods to the Problem class (these were previously duplicated identically in all 3 Result types).
The same is done for InvalidField & InvalidFieldError methods.
AddInvalidMessage methods on Result types are now obsolete and no longer throw an exception when the result is successful.
Moved AddInvalidMessage test methods for CollectionResult type to instead test directly on the Problem type.
Removed Problem.AddInvalidMessage methods added in last commit. Use the existing AddValidationError method instead which does the same thing, except safer.
All unit tests now passing again.
@KSemenenko KSemenenko merged commit 9c440d0 into managedcode:main Aug 22, 2025
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants