How to acquire Result<dynamic>?
kriewall opened this issue ยท 9 comments
What is the current behavior?
When working with dynamic objects under CSharpFunctionalExtensions, dynamic
is returned instead of Result<dynamic>
.
What is the expected behavior?
When working with dynamic objects under CSharpFunctionalExtensions, Result<dynamic>
should be returned, just as one can return List<dynamic>
. Similarly, when mapping to type T, Result should be returned.
Steps to reproduce
using System;
using System.Collections.Generic;
using CSharpFunctionalExtensions;
using FluentAssertions;
using Xunit;
namespace Locnes.Kernel.Test.CrossCutting.Extensions
{
public class ResultExtensionsLocalTest
{
[Fact]
public void Dynamic_result_can_be_cast_as_dynamic_result()
{
dynamic value = "Test";
var result = Result.Ok(value); // Expect Result<dynamic>, result is dynamic
var cast = (Result<dynamic>)result; // Fails
cast.Value.Should().Be(value);
}
[Fact]
public void Method_can_return_dynamic_result()
{
var result = GetResult(); // Fails
result.Value.Should().Be("Test");
}
[Fact]
public void Dynamic_result_can_be_cast_to_concrete_type()
{
dynamic value = "Test";
var result = Result.Ok(value)
.Map((Func<dynamic,string>)(v => (string)v)); // Result is still dynamic! Fails with error that Map does not exist
result.GetType().Should().Be(typeof(Result<string>));
}
[Fact]
public void String_result_provided_for_comparison()
{
var result = Result.Ok("Test").Map(v => (string) v);
result.GetType().Should().Be(typeof(Result<string>));
}
[Fact]
public void Method_can_return_dynamic_list()
{
var list = GetList();
list.Should().BeEmpty();
}
private Result<dynamic> GetResult() // Fails
{
dynamic value = "Test";
return Result.Ok(value);
}
private List<dynamic> GetList() // This works fine
{
return new List<dynamic>();
}
}
}
Additional info
I'm using version 1.19.1; I've not been able to make the migration to v2 yet, but I assume this has more to do with my understanding than CSharpFunctionalExtensions itself. I've found myself doing this because I need to manage different XML versions of effectively the same content; I'm attempting to do so using AmazedSaint.ElasticObject, as described here. Any suggestions how to return Result and enable mapping to concrete classes would be much appreciated. If the issue does happen to be version-specific, please let me know and I'll take the plunge to upgrade to v2.
Simply phenomenal library you folks have created here, btw.
I've changed the behavior of implicit conversions involving dynamics, should be available in v2.13.1. Let me know if it fixes your issue.
Note that extension methods won't be available on dynamic variables because C# doesn't know how to find them at runtime. You need to convert dynamic
s to Result<dynamic>
, like so:
[Fact]
public void Dynamic_result_can_be_cast_to_concrete_type()
{
dynamic value = "Test";
Result<dynamic> success = Result.Success(value); // The conversion
var result = success
.Map((Func<dynamic, string>)(v => (string)v));
result.GetType().Should().Be(typeof(Result<string>));
}
Also, the following code is valid now:
[Fact]
public void Result_of_dynamic_can_be_cast_as_dynamic_result()
{
dynamic value = "Test";
dynamic result = Result.Success(value);
var cast = (Result<dynamic>)result;
string castValue = cast.Value;
castValue.Should().Be(value);
}
Regarding the upgrade to v2, you should be able to do so without breaking changes (or with the minimum # of them). Most of the old methods are still available and are marked as obsolete.
Thanks @vkhorikov, for both the fix and the upgrade tip! I'm in the middle of some fairly intense development, but once I get to a breaking point I'll try upgrading to v2.13.1. Probably later this week or early next.
There is another implicit cast
Shouldn't fix be applied to it as well?
@hankovich Yeah, it should. Pushed an update.
@vkhorikov I got around to performing the upgrade to v2.13.2. Curiously, the one piece of functionality that broke was the content I built around dynamics. These lines of code previously had no issues:
private static dynamic GetExposure(dynamic correctionSet)
{
var appRange = Result.Ok(correctionSet.ApplicationRange);
var appRangeValidation = ResultExtensions.Ensure(appRange, (Func<dynamic, bool>)(applicationRange => applicationRange != null), "ApplicationRange may not be null");
var exposure = ResultExtensions.Map(appRangeValidation, (Func<dynamic, dynamic>) (applicationRange => applicationRange.Exposure));
var exposureValidation = ResultExtensions.Ensure(exposure, (Func<dynamic, bool>) (e => e != null), "Exposure may not be null");
return exposureValidation;
}
But the block now fails with the following error: Microsoft.CSharp.RuntimeBinder.RuntimeBinderException : The call is ambiguous between the following methods or properties: 'CSharpFunctionalExtensions.ResultExtensions.Ensure<object,string>(CSharpFunctionalExtensions.Result<object,string>, System.Func<object,bool>, string)' and 'CSharpFunctionalExtensions.ResultExtensions.Ensure<MyApp.Core.Application.Runjobs.RequestSettings.ExternalDependency.v1_5_1.CorrectionSetType_ApplicationRange_type>(CSharpFunctionalExtensions.Result<MyApp.Core.Application.Runjobs.RequestSettings.ExternalDependency.v1_5_1.CorrectionSetType_ApplicationRange_type>, System.Func<MyApp.Core.Application.Runjobs.RequestSettings.ExternalDependency.v1_5_1.CorrectionSetType_ApplicationRange_type,bool>, string)'
One note: The casting of delegates to Func<dynamic, xxx> was necessary to get the code to run originally; apparently the compiler can't figure out the delegate types when using dynamics. But you probably knew that already! (Oh, and please do forgive my crude use of extension methods; I'll update them once I get around to casting as Result).
Any suggestions what I can do to resolve this issue?
Also, related to the upgrade, there was one non-blocking item for which I wanted to request your input... the insertion of the ValueObject collection Fluent Assertions extension method seem to require me to cast custom collection types as IEnumerable in order to avoid ambiguous call references. In the example below, I can use an as statement, but other places required a hard cast. I think I will probably need to create my own extension methods tailored to my custom collections; is that the best way to avoid the redundant casts?
[Fact]
public void Multiplication_multiplies_collection_with_term_weight()
{
var settings = ObjectMother.GetWaferSettings();
var weight = ObjectMother.GetUniformWeightsByTerm(weightValue: 2);
var product = settings * weight as IEnumerable<Setting>;
var expected = settings.Select(s => new Setting(s.TermName, s.Value * 2));
product.Should().BeEquivalentTo(expected);
}
Appreciate if you could provide guidance, especially for the first item.
Could you post the underlying class of the correctionSet
parameter?
Regarding ambiguous calls with Fluent Assertions, what is the type of the product
? Fluent Assertions should pick up the most specific type (it's a C# feature actually, not the library's). But overall, yes, custom extension methods help resolve a lot of issues with Fluent Assertions (or just simplify the code).
Content below, minorly scrubbed - nothing fancy, it's just a C# class autogenerated from an XSD.
[System.CodeDom.Compiler.GeneratedCodeAttribute("xsd", "4.6.1055.0")]
[System.SerializableAttribute()]
[System.Diagnostics.DebuggerStepThroughAttribute()]
[System.ComponentModel.DesignerCategoryAttribute("code")]
[System.Xml.Serialization.XmlTypeAttribute(Namespace = "http://www.dummy.com/XMLSchema/MT/Generic/ExternalDependency/v1.5")]
public partial class CorrectionSetType
{
private string correctionSetNameField;
private string correctionSetType1Field;
private CorrectionSetType_ApplicationRange_type applicationRangeField;
private ParameterType parametersField;
private string[] referencedSubRecipesField;
public CorrectionSetType()
{
this.correctionSetNameField = "default";
this.correctionSetType1Field = "defaultType";
}
/// <remarks/>
[System.Xml.Serialization.XmlElementAttribute(Form = System.Xml.Schema.XmlSchemaForm.Unqualified)]
public string CorrectionSetName
{
get
{
return this.correctionSetNameField;
}
set
{
this.correctionSetNameField = value;
}
}
/// <remarks/>
[System.Xml.Serialization.XmlElementAttribute("CorrectionSetType", Form = System.Xml.Schema.XmlSchemaForm.Unqualified)]
public string CorrectionSetType1
{
get
{
return this.correctionSetType1Field;
}
set
{
this.correctionSetType1Field = value;
}
}
/// <remarks/>
[System.Xml.Serialization.XmlElementAttribute(Form = System.Xml.Schema.XmlSchemaForm.Unqualified)]
public CorrectionSetType_ApplicationRange_type ApplicationRange
{
get
{
return this.applicationRangeField;
}
set
{
this.applicationRangeField = value;
}
}
/// <remarks/>
[System.Xml.Serialization.XmlElementAttribute(Form = System.Xml.Schema.XmlSchemaForm.Unqualified)]
public ParameterType Parameters
{
get
{
return this.parametersField;
}
set
{
this.parametersField = value;
}
}
/// <remarks/>
[System.Xml.Serialization.XmlArrayAttribute(Form = System.Xml.Schema.XmlSchemaForm.Unqualified)]
[System.Xml.Serialization.XmlArrayItemAttribute("elt", Form = System.Xml.Schema.XmlSchemaForm.Unqualified, IsNullable = false)]
public string[] ReferencedSubRecipes
{
get
{
return this.referencedSubRecipesField;
}
set
{
this.referencedSubRecipesField = value;
}
}
}
Regarding the type of product
, it is of type WaferSettings<SomeSpecificSetting>
, where:
public class WaferSettings<T> : IEnumerable<T>, IAmWaferSpecific, IEquatable<WaferSettings<T>> where T : Setting
{
...
}
It didn't work because we've added Result<T, E> in v2. Try this one, the difference is that here, you specify the number of arguments in the Result
explicitly:
private static dynamic GetExposure(dynamic correctionSet)
{
Result<dynamic> appRange = Result.Ok<dynamic>(correctionSet.ApplicationRange);
var appRangeValidation = ResultExtensions.Ensure(appRange, (Func<dynamic, bool>)(applicationRange => applicationRange != null), "ApplicationRange may not be null");
var exposure = ResultExtensions.Map(appRangeValidation, (Func<dynamic, dynamic>)(applicationRange => applicationRange.Exposure));
var exposureValidation = ResultExtensions.Ensure(exposure, (Func<dynamic, bool>)(e => e != null), "Exposure may not be null");
return exposureValidation;
}
@vkhorikov Sorry for the delay, but: it works! Thanks for the keen eye. :) I'll go ahead and close this out.