25

I opened our solution in Visual Studio 2015 yesterday and a few of our unit tests (which ran fine in Visual Studio 2013) starting failing. Digger deeper I discovered it was because calling GetTypes() on an assembly was returning different results. I've been able to create a very simple test case to illustrate it.

In both Visual Studio 2013 and 2015 I created a new console application using .NET Framework 4.5.2. I put the following code in both projects.

class Program
{
    static void Main(string[] args)
    {
        var types = typeof(Program).Assembly.GetTypes()
                .Where(t => !t.IsAbstract && t.IsClass);

        foreach (var type in types)
        {
            Console.WriteLine(type.FullName);
        }

        Console.ReadKey();
    }
}

When I run in Visual Studio 2013 I get the following output (as expected).

VS2013Example.Program

When I run in Visual Studio 2015 I get the following output (not as expected).

VS2015Example.Program

VS2015Example.Program+<>c

So what is that VS2015Example.Program+<>c type? Turns out it's the lambda inside the .Where() method. Yes, that's right, somehow that local lambda is being exposed as a type. If I comment out the .Where() in VS2015 then I no longer get that second line.

I've used Beyond Compare to compare the two .csproj files but the only differences are the VS version number, the project GUID, names of the default namespace and assembly, and the VS2015 one had a reference to System.Net.Http that the VS2013 one didn't.

Has anyone else seen this?

Does anyone have an explanation as to why a local variable would be being exposed as a type at the assembly level?

Community
  • 1
  • 1
Craig W.
  • 17,838
  • 6
  • 49
  • 82

1 Answers1

29

Has anyone else seen this?

Yes, this is caused by the new compiler behavior for lifting lambda expressions.

Previously, if a lambda expression didn't capture any local variables, it would be cached as a static method at the call site, which made the compiler team need to jump some hoops in order to properly align the method arguments and the this parameter. The new behavior in Roslyn is that all lambda expressions get lifted into a display class, where the delegate is exposed as an instance method in the display class, disregarding if it captures any local variables.

If you decompile your method in Roslyn, you see this:

private static void Main(string[] args)
{
    IEnumerable<Type> arg_33_0 = typeof(Program).Assembly.GetTypes();
    Func<Type, bool> arg_33_1;
    if (arg_33_1 = Program.<>c.<>9__0_0 == null)
    {
        arg_33_1 = Program.<>c.<>9__0_0 = 
                        new Func<Type, bool>(Program.<>c.<>9.<Main>b__0_0);
    }
    using (IEnumerator<Type> enumerator = arg_33_0.Where(arg_33_1).GetEnumerator())
    {
        while (enumerator.MoveNext())
        {
            Console.WriteLine(enumerator.Current.FullName);
        }
    }
    Console.ReadKey();
}

[CompilerGenerated]
[Serializable]
private sealed class <>c
{
    public static readonly Program.<>c <>9;
    public static Func<Type, bool> <>9__0_0;
    static <>c()
    {
        // Note: this type is marked as 'beforefieldinit'.
        Program.<>c.<>9 = new Program.<>c();
    }
    internal bool <Main>b__0_0(Type t)
    {
        return !t.IsAbstract && t.IsClass;
    }
}

Where's with the old compiler, you'd see this:

[CompilerGenerated]
private static Func<Type, bool> CS$<>9__CachedAnonymousMethodDelegate1;

private static void Main(string[] args)
{
    IEnumerable<Type> arg_34_0 = typeof(Program).Assembly.GetTypes();
    if (Program.CS$<>9__CachedAnonymousMethodDelegate1 == null)
    {
        Program.CS$<>9__CachedAnonymousMethodDelegate1 = 
                            new Func<Type, bool>(Program.<Main>b__0);
    }
    IEnumerable<Type> types =
                arg_34_0.Where(Program.CS$<>9__CachedAnonymousMethodDelegate1);

    foreach (Type type in types)
    {
        Console.WriteLine(type.FullName);
    }
    Console.ReadKey();
}

[CompilerGenerated]
private static bool <Main>b__0(Type t)
{
    return !t.IsAbstract && t.IsClass;
}

You can get the desired result by filtering out classes that have the CompilerGenerated attribute attached to them:

var types = typeof(Program)
            .Assembly
            .GetTypes()
            .Where(t => !t.IsAbstract && 
                         t.IsClass && 
                         Attribute.GetCustomAttribute(
                            t, typeof (CompilerGeneratedAttribute)) == null);

For more, see my question Delegate caching behavior changes in Roslyn

Community
  • 1
  • 1
Yuval Itzchakov
  • 146,575
  • 32
  • 257
  • 321
  • 1
    Thanks for the info. Seems a little scary because it feels like a change that's likely to cause a lot of existing code that's been working just fine to suddenly exhibit bugs. Over the years I've lost count of the number of times I've written code that enumerates over the types in an assembly. Feels like `GetTypes()` should maybe have an overload that lets the developer explicitly state if they want the compiler-generated types included. – Craig W. Jul 21 '15 at 16:51
  • @CraigW. Should be pretty easy to write an extension method for that but i completely agree it's a potential breaking change because even with an extension method it wouldn't be called by default, maybe you should file an issue with the Roslyn team on github? – Ron Beyer Jul 21 '15 at 17:57
  • @Craig This isn't a breaking change, that is an ***implementation detail***. If you had captured a variable inside your delegate you'd see the same behavior. – Yuval Itzchakov Jul 21 '15 at 18:21
  • @YuvalItzchakov Nice! How did you do the "decompilation" with roslyn exactly? – Ties Jul 21 '15 at 20:56
  • 4
    @YuvalItzchakov: We'll have to agree to disagree on whether it's a breaking change. It broke my code. :-) – Craig W. Jul 22 '15 at 13:58
  • @CraigW. Your code is relying on an implementation detail. Therefore, your code was already broken to begin with. – Tom Lint Feb 24 '16 at 12:37
  • @TomLint: Thanks for chiming in seven months later. But as with the other poster, we'll have to agree to disagree on this one. There's no way someone writing code like we had in place would know there was an "implementation detail" they were depending upon. If I were using reflection to call undocumented methods on framework objects I would agree with this assessment. The compiler magically exposing new types that weren't there before is more than a simple implementation detail. – Craig W. Feb 24 '16 at 17:24
  • 1
    @CraigW *The compiler magically exposing new types that weren't there before is more than a simple implementation detail.* That's not really true. The fact that the compiler now *generates* a display class instead of static method at the call site is an implementation detail. If your code had any clojure over some local value, you would of seen this bug earlier when only writing the code. When you use `Assembly.GetTypes`, you do *expect* to see all all classes in the assembly, even if they're compiler generated. – Yuval Itzchakov Feb 24 '16 at 17:33