-
Notifications
You must be signed in to change notification settings - Fork 301
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Result<E> implementation #200
Comments
I ran into this as well. It's holding me back from using the package in my production software. |
One more vote, I have solved this currently with 2 extension methods, to serialize an array of errors as json and back... |
Usually we add something like public readonly struct Unit
{
public static Unit Instance => default;
} to represent a missing value and use |
A while back I created a lightweight Result library - initially as a clean-slate experimental/sandbox project, with the goal of feeding back my experiences here. I wanted to see how the codebase could evolve without the need for backwards compatibility, and it was much easier to try new things without every change breaking the large existing codebase. I ended up implementing: Plus, for convenience, the string-error types: In this way, client code benefits from simpler method definitions (rather than The naming rationale: I really want to backport my work here, but other commitments have prevented this. In any case, I have only implemented the bare minimum functionality to meet my project requirements, so only a fraction of all the methods. Assuming the name The other backwards-compatible option is to implement |
I like the idea of using
Why does the existing |
No, no, I said: "the existing Result type does not hold a value" 😂 |
Oh, makes sense, I thought it was a typo :) So what would be an alternative to |
@space-alien Could you please make the lightweight Result library public? I will work on backporting it here. It's time to introduce |
Apologies for the slow response; I am travelling at the moment so not a lot of time for detailed replies. Implementing It will also significantly increase the maintenance and documentation burden of the library. Its addition will also increase complexity for users (e.g. navigating IntelliSense, in particular where So this is quite a big thing, and before embarking upon it I think we should make sure it's the right way forward (and if so, that the name is chosen correctly).
Yes, but I don't think this should be added to the current codebase (deprecating
I'll try to publish/share it soon. |
Didn't mean that to sound negative by the way.. I agree that this is a gap, just keen to make sure we get it right :) |
Yeah, I don't know what the right is either. And the fact that I haven't used the library's Result in my last couple projects doesn't help it. @hankovich If you could share your opinion on the topic of In any case, I can start working on backporting it, and it should become clearer whether this is the right way forward as I go along. |
@vkhorikov yeap, I've posted some of my thoughts here and will duplicate them here as well. Example: resultUnitError.Map(unit => 42) // this call does not look clean enough for me since we can't do anything with unit anyway
unitResultError.Map(() => 42) // looks clean indeed Nobody says we should add both UnitResult and all extensions/overloads for it at the same time, it will be incremental work. Since we came up to this problem 4 years after the 'launch' of the library, I don't expect a lots of users will use But I want to see handy extensions for those who will (like me). Does this make sense to you guys? |
Yes, I agree in principle, especially with the incremental approach. Before starting, I think we should plan the work carefully and perhaps add a tracking issue with a list work items so that everyone can see the status of the implementation. In the meantime, I have pushed a new branch with an implementation |
@space-alien at that time I see this draft PR was updated by @c17r and it now contains its own implementation of |
@hankovich @c17r Ah right, I didn't review the code since the discussion on the PR changed direction away from interfaces. I will take a look. |
I also agree, and I like the idea with the incremental approach. I also like the idea to come up with a specific work plan and a tracking issue. I guess GitHub's Projects tab would be a good fit here. Any ideas of how to best organize it? |
Are there any updates to this? We have started using this package in our product with a custom error interface IMyError, and using the Result<Unit,IMyError> workaround, but previously clean statements all become quite a bit more convoluted now. Where previously |
@Chris197 This is still in the plans. I plan to do a bare-bones version within the next month or two (with basic extension methods) and hopefully people will be able to fill in missing extension methods eventually. |
@Chris197 Could you share your approach with the |
@vkhorikov Sure, I've put together a rough example of how we are more or less approaching this: A few things to note:
Any comments are welcome of course. |
Thanks @Chris197 . The main comment I have is that handling the input field names like in
is not the error's responsibility because they are application / data contract concerns. That would be fine on its own (as a small concession of purity toward simpler code) but it doesn't work if the same value object is represented by different field names in data contracts. |
@vkhorikov Yes, agreed, not quite sure what the best approach is yet. I could initially leave field empty, but that would mean creating an incomplete error object, relying on the caller to fill it in, which is not great. Alternatively I could remove the field name from the error object and then have the caller wrap it in a new 'ErrorWithField' model afterwards. Any suggestions? |
I use a similar concept in a recent project where I have a KeyedError class that has key/error pairs and can hold multiple errors. The keys are obviously specific to the function producing the Result. My way of handling the 'purity' issue is that it is the responsibility of the calling function to map the keys to ones that make sense for it's own context. e.g. public static Result<Something> CreateSomething(SomethingDto dto) {
return FailureIf(dto.Name == null, new KeyedError(nameof(dto.Name), Errors.CannotBeNull))...
}
public Result CreateAndSaveSomething(string name) {
var dto = new SomethingDto(name);
return CreateSomething(dto).MapKey(nameof(dto.Name), nameof(name)).Tap(s => repo.Save(s));
} The ultimate caller (perhaps an asp.net form or API endpoint) than has a validation response for fields in its own context. Interested to know thoughts on this approach? (Note in practice MapKey would take in some sort of dictionary/mapping function for larger objects) |
@Chris197 yes, mapping of the error to a specific field in the dto/data contract should be done by the app layer (controller). In ASP.NET, you can usually rely on the built-in mapping mechanism. Check out this article: https://enterprisecraftsmanship.com/posts/combining-asp-net-core-attributes-with-value-objects/ |
@rathga looks fine given that |
@vkhorikov Thanks for that! What about cases of more complex validation/business logic beyond value objects that require loading an aggregate? e.g. checking an amount is below a deposit balance for customers that are not permitted credit. Would you then revert to something like the mapping mechanism I described? |
@rathga Yes, for more complex validations (that are handled by the controller), you'd need to do the field mapping manually in the controller. |
Any updates on the UnitResult? Or how should I return a Result with no value but error class? Thanks. |
Still working on this. I was hoping to release it with all the extension methods, but will probably do it in chunks. Will publish the class itself this weekend. |
Great! It is a good idea to do it in chunks. Just the class itself is enough for me right now. I knew CSharpFunctionalExtensions from your excellent Pluralsight courses. I like the idea that returning a Result class instead of a bool or string, and having a specific Error class for each error. If I understand the courses correctly, if a method is a query then I should return Result<T, E> and if a method is a command I should return Result, but of course, there is no Result. That was why I found this GitHub issue. So if a method is a command I should return UnitResult (or Result<Unit, E>, depending on which way you choose), right? |
Yes, that's correct. Queries return Result<T, E>, commands -- Result<Unit, E> or UnitResult. Note that if you are working with decorators (ala CQRS), it would be easier to work with Result<Unit, E> over UnitResult because of the "compatibility" between Result<T, E> and Result<Unit, E>. Result is just a shortcut for Result<Unit, string> |
Thanks for clarifying that. Another question is that the CSharpFunctionalExtensions has evolved a lot since you released your courses on Pluralsight. Are there any articles or courses that introduce how to use this library in the correct way, especially all those extension methods? |
I plan to do an extensive documentation for that, but no ETA for that just yet. I'm also going to update the Pluralsight course at some point. |
Sounds great! Looking forward to them! |
I've added a basic implementation of |
Hi,
Is there any plans for implementing Result class?
(Result with no value but error class)
The text was updated successfully, but these errors were encountered: