5

My API returns an object, populated with an anonymous type. I can GET it through HTTP and it resolves to a json.
But... my unit test for the same method cannot resolve the anonymous type.
Dotnet framework 4.7.2 is used.

The code below shows the caller, in one project
and the callee, in another project,
with 2 different, failed, attempts. The second is copied from SO and here and there is no comment it doesn't work. But... it doesn't for me.

The error message is:

System.InvalidCastException : Unable to cast object
of type '<>f__AnonymousType1`2[System.String,System.String]'
to type '<>f__AnonymousType0`2[System.String,System.String]'.

where one can see the types are different.
But then IIRC there was a dotnet update some time ago that solved this issue to let them be "equal".


I am trying to resolve a use case by returning an anonymous type from a dotnet framework web api.
(there are other solutions, like dynamic, reflection, types or more endpoints but that is another question)
Note that HTTP is not involved in this test case, it is purely a dotnet framework issue.

WebAPI (project 1):

public class MyController: ApiController
{
    public object Get(string id)
    {
        return new { Key = id, Value = "val:" + id };
    }
}

Test (project 2):

[Fact]
public void MyTest
{
    var sut = new MyController();

    // Act.
    dynamic res = sut.Get("42");

    // Assert.

    // First attempt:
    Assert.Equal("42", res.Key);

    // Second attempt:
    var a = new { Key = "", Value = "" };
    a = Cast(a, res);  // <- the code fails already here

    Assert.Equal("42", a.Key);
}

private static T Cast<T>(T typeHolder, object x)
{
    // typeHolder above is just for compiler magic
    // to infer the type to cast x to
    return (T)x;
}


Community
  • 1
  • 1
LosManos
  • 7,195
  • 6
  • 56
  • 107
  • you shouldn't return an anonymous type from an API controller in general – Marco Salerno Jan 23 '20 at 09:24
  • 1
    I've just tried to do like [Tomas said](http://tomasp.net/blog/cannot-return-anonymous-type-from-method.aspx/) and it works. I used VS2019 and netcore 3.1 framework – oleksa Jan 23 '20 at 09:32
  • @oleksa I am forced to use the full framwork. Maybe it was only implemented in core then. Sounds reasonable. – LosManos Jan 23 '20 at 09:55
  • I have good (bad) news. I did the same Tomas code on VS 2017 .Net Framework 4.8 and it works like a charm. – oleksa Jan 23 '20 at 10:00
  • could you please check [the code from gist](https://gist.github.com/oleksabor/b1e186be0b80f5e6f64c585535315d8f) in the new project ? It works on my machine with .net 4.7.2 – oleksa Jan 23 '20 at 11:32
  • @oleksa Aha. I forgot to mention that it is two different projects, one testee and one tester. In the same project it works. I will update the question. – LosManos Jan 23 '20 at 11:45
  • 1
    Does this answer your question? [Return/consume dynamic anonymous type across assembly boundaries](https://stackoverflow.com/questions/2993200/return-consume-dynamic-anonymous-type-across-assembly-boundaries) – oleksa Jan 23 '20 at 12:23
  • @oleksa That is the thing. When I wrote the (your) code in the same project I realised the assembly boundary crossing what the culprit and then the answer was "it cannot presently be done in this way". (Reflection solves the problem.) Feel free to write an answer that it cannot be done over assembly boundaries and link the URL you suggested so I can mark your answer as Answer. – LosManos Jan 23 '20 at 13:08
  • Glad that you have resolved it. However I agree with @MarcGravell that it is better to return strong typed dto object from the controller method rather then anonymous class instance. – oleksa Jan 23 '20 at 14:09

2 Answers2

1

Anonymous types aren't good for any scenario involving type testing, especially between assemblies. In some scenarios value-tuples (or the class tuples, since a value-tuple would be boxed here anyway) might be useful to you, or you could define a real proper type.

Otherwise: it is fine to test anonymous types (or any types) by looking at the shape with reflection (does it have features X/Y/Z?) - but without doing a test on the actual runtime type.

Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
1

The conversion fails because when you try to cast anonymous types, both instances have to be from the same anonymous type. Even if they have the same type of properties it won't work, the cast cannot be performed, just like if you had

public class A
{
    public string SomeProperty { get; set; }
    public string SomeOtherProperty { get; set; }
}

public class B
{
    public string SomeProperty { get; set; }
    public string SomeOtherProperty { get; set; }
}

And would try to do something like (B)instanceOfA.

If possible, you should probably use tuples instead

nalka
  • 1,894
  • 11
  • 26
  • Dotnet handling the anonymous classes as different types, as you mention, was my immediate reaction. But then I stumbled upon too many solution like [this](http://tomasp.net/blog/cannot-return-anonymous-type-from-method.aspx/) which said that it was possible anyway. Was the duck typing described never implemented? – LosManos Jan 23 '20 at 09:26
  • @LosManos Are both the WebAPI and the test compiled at same time before you run the test? – nalka Jan 23 '20 at 09:44
  • Yes. Same Visualstudio solution. Dotnet framework 4.7.2. – LosManos Jan 23 '20 at 09:54
  • Then i'm afraid I can't understand why it generates two different anonymous types. It might be because of `dynamic`... – nalka Jan 23 '20 at 10:00