I have a generic method called Transfer
which returns a generic type too. In the body of Transfer
, I do some processing and get MessageBundle
object. How Can I reconstruct the return type of Transfer
(using MessageBundle
object) in a generic way?
I can only make it work in non-generic way.
namespace ConsoleApp
{
internal class Program
{
static void Main(string[] args)
{
//this works
var test = Transfer<(string? Result, string? Error)>();
//???
var test = Transfer<(int? Result, string? Error)>();
var test = Transfer<(bool? Result, string? Error)>();
var test = Transfer<(object? Result, string? Error)>();
}
public static U Transfer<U>()
{
//getting MessageBundle object
var bundle = new MessageBundle
{
Result = "hi",
Error = null
};
//How to reconstruct in generic way?
(string? Result, string? Error) response = (bundle.Result as string, bundle.Error);
return (U)Convert.ChangeType(response, typeof(U));
}
}
public class MessageBundle
{
public string? Error { get; set; }
public object? Result { get; set; }
}
}
Edit 1: I have no control over the signature of Transfer
method.
Edit 2: The shape of U
(Transfer
method) will be a tuple of two elements (Error
is always a string while Result
can vary)
CodePudding user response:
If you know in advance that U
will be a tuple of 2 items but cannot change the signature of public static U Transfer<U>()
in any way (even by adding an ITuple
constraint), you will need to use reflection to construct the tuple. And assuming that the first tuple parameter is a .NET primitive of some sort and that the value of Result
is its string representation, you can use Convert.ChangeType()
to convert the string value to the final value.
One way to do this would be:
if (!typeof(ITuple).IsAssignableFrom(typeof(U)) // Assert it's a tuple
|| !(typeof(U).IsGenericType && typeof(U).GetGenericArguments() is var arguments && arguments.Length == 2) // Assert it has exactly two generic parameters
|| arguments[1] != typeof(string)) // Assert that the second parameter is a string
throw new ArgumentException(string.Format("Unexpected type {0}", typeof(U)));
var tuple = (U)Activator.CreateInstance(typeof(U),
bundle.Result == null ? null : Convert.ChangeType(bundle.Result, Nullable.GetUnderlyingType(arguments[0]) ?? arguments[0]),
bundle.Error)!;
Notes:
You don't specify what should happen in the event the
Result
conversion fails, e.g. you are specifying an integer type forResult
but the value is"hi"
. The implementation above will throw an exception from withinConvert.ChangeType()
if this happens.You also don't specify what should happen when the
Result
corresponds to some complex object that does not implementIConvertible
. The implementation above will also throw an exception in such a case.Convert.ChangeType(object, Type)
uses the current culture to do the conversion. If you want to use the invariant culture (e.g. because you are deserializing a value received over the wire) useChangeType(Object, Type, CultureInfo.InvariantCulture)
:var tuple = (U)Activator.CreateInstance(typeof(U), bundle.Result == null ? null : Convert.ChangeType(bundle.Result, Nullable.GetUnderlyingType(arguments[0]) ?? arguments[0], CultureInfo.InvariantCulture), bundle.Error)!;
You really should rethink this design. Using reflection in this manner eliminates almost all compile-time checking for code correctness.
Demo fiddle here.