我需要在C#中存根一个异步泛型方法。live方法调用REST API并使用Newtonsoft JSON将结果取消序列化为适当的格式,该格式将是各种类型的列表。
然而,试图将其截断会遇到各种问题。下面的主代码将失败,因为它没有返回Task。
public Task<T> ExportReport<T>(string reportName)
{
if(reportName.Contains("personlookup "))
{
List<person> people = new();
people.Add(new person { Forenames = "Bob", Surname = "Brown" });
return people;
}
else if (reportName.Contains("GetOrder"))
{
List<order> orders = new();
orders.Add(new order { ItemType = “Apples”, ID = "1234" });
return orders;
}
else
{
return default;
}
}
我尝试过各种各样的东西,比如Task。FromResult((并将该方法设置为async等,到目前为止,唯一尝试过的甚至可以编译的是
return (Task<T>)people.Cast<T>();
它在运行时失败,出现无效的强制转换异常。我是不是做错了什么,或者这种方法是不可能的。
了解问题
问题不在于异步泛型函数,而在于一般的泛型函数。您有一个返回类型为Task<T>
的对象的泛型函数,其中T
可以是任何类型。T的类型由调用者决定,而不是由被调用的方法决定。您正试图截断此函数并返回一个特定的类型。想象一下,函数是这样声明的:
public T ExportReport<T>(string reportName)
由于同样的原因,你也会面临同样的问题,即如何存根。
测试&重构
事实上,你正在存根意味着你正在测试(尽管你可能不是(。有时测试会发现我们的代码存在设计缺陷。例如,在输入参数(字符串(上分支以返回不同类型对象的泛型方法可能不是最佳解决方案。
假设参数"a"
返回A
类型的对象,参数"b"
返回B
类型的对象。所以A myA = ExportReport("a")
返回一个A
,但是当您键入A myA = ExportReport("b")
时会发生什么?因为它要编译,你会在某个地方遇到问题。
通过使这个函数通用,你真的取得了什么成就吗?
也许你应该从你的测试经验中得到这个提示,并考虑重构?也许您可以有几个不同的非泛型函数,每个函数都返回不同的报告类型?
Nasty解决方案(.NET5.0或更高版本(
如果你不在乎这些,只想让它发挥作用,你可以做这样的事情:
public T ExportReport<T>(string reportName)
{
if (reportName.Contains("personlookup "))
{
List<Person> people = new();
people.Add(new Person { Forenames = "Bob", Surname = "Brown" });
return Unsafe.As<List<Person>,T>(ref people);
}
throw new ArgumentOutOfRangeException(nameof(reportName));
}
以及"任务版本:">
public Task<T> ExportReport<T>(string reportName)
{
if (reportName.Contains("personlookup "))
{
List<Person> people = new();
people.Add(new Person { Forenames = "Bob", Surname = "Brown" });
var peopleTask = Task.FromResult(people);
return Unsafe.As<Task<List<Person>>,Task<T>>(ref peopleTask);
}
throw new ArgumentOutOfRangeException(nameof(reportName));
}
更好的解决方案
听起来您可以使用常规JSON转换器序列化存根数据并对其进行去验证,这也解决了问题。
最佳解决方案
可能是重构。
来自注释:
它不用于单元测试。我只是试图将它与内部API服务器解耦,这样我就可以脱机处理UI元素。
考虑到的目的有限,那么我想你可以这样做:
// Using `typeof(IEnumerable<T>)` instead of `typeof(List<T>)` or `typeof(IList<T>)` because it means `IsAssignableFrom` will work with `List<T>`, `T[]`, `IReadOnlyList<T>`, `IList<T>`, `ImmutableList<T>`, etc.
// It's maddening that even in .NET 6, `IList<T>` still does not extend `IReadOnlyList<T>`. Grumble.
private static readonly _typeofIEnumerablePerson = typeof(IEnumerable<Person>);
private static readonly _typeofIEnumerableOrder = typeof(IEnumerable<Order>);
public Task<T> ExportReportAsync<T>(string reportName)
{
T list = GetStubList<T>();
return Task.FromResult( list );
}
private static TList GetStubList<TList>()
{
if( _typeofIEnumerablePerson.IsAssignableFrom( typeof(TList) ) )
{
List<Person> people = new List<Person>()
{
new Person { Forenames = "Bob", Surname = "Brown" }
};
return ToTListWorkaround<TList>( people );
}
else if( _typeofIEnumerableOrder.IsAssignableFrom( typeof(TList) ) )
{
List<Order> orders = new List<Order>()
{
new Order { ItemType = "Apples", ID = "1234" }
};
return ToTListWorkaround<TList>( orders );
}
else
{
throw new NotSupportedException( "Unsupported list type: " + typeof(TList).FullName );
}
}
private static TList ToTListWorkaround<TList>( IEnumerable actual )
{
// Ugly workaround:
Object asObject = actual;
TList returnValue = (TList)asObject;
return returnValue;
}