A few months ago I decided I wanted to bring some Reactive Extensions (Rx) awesomeness into some of my custom workflow activities. So, as any good .NET dev would do, I fired up the Package Manager Console and typed
Install-Package Rx-Main and it installed Rx 2.1 for me. Then I started leveraging Rx APIs in my custom activity implementation and all seemed well… at first. At some point I had closed out of the solution and when I re-opened the solution I suddenly had no Intellisense for any types except those belonging to the mscorlib assembly, yet I could build the project no problem. I thought to myself “Wait, what? This was just working? What happened?”. After several attempts at trying to figure out what had gone wrong, I realized that it was adding the Rx library to the project caused the problem. At first I thought maybe it was something specific about Rx itself, but I knew that couldn’t be it. Then I realized that Rx was a PCL lib (note: 2.1 was at the time, 2.2 now adds a .NET 4.5 specific assembly too). So I installed another PCL lib just to check and, sure enough, it caused the same problem.
Next, as a sanity check, I created a brand new WF Console Application to make sure it had nothing to do with my specific solution/projects. As soon as I added a PCL lib, close and reopened the solution I lost Intellisense. So, I did what any frustrated Visual Studio user should do and I opened a bug on Connect. Unfortunately, that bug was promptly closed as “By Design” with a comment that it would be addressed in a future version of Visual Studio.
Now, I’d like to think I’m pretty good at memorizing APIs, but with the plethora of libraries that one works with these days, not to mention all the overloads, extensions, optional parameters, generic signatures, etc. it’s a very tall order to code at max efficiency without Intellisense support. This bug effectively breaks one of the key features of Visual Studio and they aren’t going to commit to fixing it in the next update… if not sooner? Yeah, I wasn’t satisfied with that response. This basically meant I would have to choose between throwing out WF and finding some other tech to solve those problems or not being able to leverage PCL libs. The investment in WF was too large to just toss (not to mention I personally find WF to be an awesome technology) and with PCL’s becoming more and more prevalent, even from MS groups, this simply wasn’t a choice I could make. So, back against the wall, I spent about an hour yesterday debugging the root cause and coming up with a workaround.
First thing I did was fire up two instances of Visual Studio 2013. The first instance would be where I open the Workflow Console Application project that I had been repro’ing the issue with and the second instance would be attached as a debugger of the first instance. Next I turned off the “Just My Code” feature in the debugger instance and configured to break on all CLR first chance exceptions. Then I popped over to the other instance and opened up the Workflow Console Application project. After stepping through a plethora of benign exceptions from various sources, I started to see an exception saying that the System.Runtime assembly could not be located. Specifically, I saw this:
A first chance exception of type 'System.IO.FileLoadException' occurred in System.Xaml.dll
Additional information: Cannot resolve dependency to assembly 'System.Runtime, Version=220.127.116.11, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' because it has not been preloaded. When using the ReflectionOnly APIs, dependent assemblies must be pre-loaded or loaded on demand through the ReflectionOnlyAssemblyResolve event.
Hmm, well, I know
System.Runtime is type fwd’d to mscorlib in .NET4.5, but why the heck wouldn’t that “just work”? Oh, wait… what’s that bit about ReflectionOnly? So I turned my attention to the stack trace:
System.Xaml.dll!System.Xaml.XamlSchemaContext.TryGetCompatibleXamlNamespace(string xamlNamespace, out string compatibleNamespace) Unknown
System.Xaml.dll!System.Xaml.XamlXmlReader.IsXmlNamespaceSupported(string xmlNamespace, out string newXmlNamespace) Unknown
System.Xaml.dll!System.Xaml.XmlCompatibilityReader.MapNewNamespace(string namespaceName) Unknown
System.Xaml.dll!System.Xaml.XmlCompatibilityReader.GetMappedNamespace(string namespaceName) Unknown
System.Xaml.dll!System.Xaml.XmlCompatibilityReader.ReadStartElement(ref bool more) Unknown
System.Xaml.dll!MS.Internal.Xaml.NodeStreamSorter.NodeStreamSorter(MS.Internal.Xaml.Context.XamlParserContext context, MS.Internal.Xaml.Parser.XamlPullParser parser, System.Xaml.XamlXmlReaderSettings settings, System.Collections.Generic.Dictionary
System.Xaml.dll!System.Xaml.XamlXmlReader.Initialize(System.Xml.XmlReader givenXmlReader, System.Xaml.XamlSchemaContext schemaContext, System.Xaml.XamlXmlReaderSettings settings) Unknown
System.Xaml.dll!System.Xaml.XamlXmlReader.XamlXmlReader(System.Xml.XmlReader xmlReader, System.Xaml.XamlSchemaContext schemaContext, System.Xaml.XamlXmlReaderSettings settings) Unknown
XamlBuildTask.dll!Microsoft.Build.Tasks.Xaml.PartialClassGenerationTaskInternal.ReadXamlNodes(string xamlFileName) Unknown
XamlBuildTask.dll!Microsoft.Build.Tasks.Xaml.PartialClassGenerationTaskInternal.ProcessMarkupItem(Microsoft.Build.Framework.ITaskItem markupItem, System.CodeDom.Compiler.CodeDomProvider codeDomProvider) Unknown
[AppDomain (DefaultDomain, #1) -> AppDomain (PartialClassAppDomain_ba752eb5-e2fa-4aec-b2e9-5b9713fbf23d, #8)]
Now, as you probably know, Workflow is based on XAML and so what we see here is the XAML compiler trying to do its job. One of the things it does is create a “reflection only”
AppDomain where it attempts to load/validate all the types in the XAML document. The other part of the error message mentions that the domain needs to be pre-initialized with all the assemblies that will be referenced OR someone has to resolve the assembly themselves by hooking the
ReflectionOnlyAssemblyResolve event. First I peeked to see if something had hooked that event, but the delegate was null, so I knew that the XAML compiler must have been pre-loading the assemblies. So then I looked at the assemblies that were pre-loaded by calling
AppDomain::ReflectionOnlyGetAssemblies and, guess what? No
System.Runtime. How does the XAML compiler know what list of assemblies to pre-load? By the assembly references you have in your project.
As mentioned earlier, in .NET 4.5, there is no need to reference
System.Runtime any more because the types are all in
mscorlib now, so if you start a new 4.5 project (or upgrade an existing project to 4.5), there will be no reference to
System.Runtime any more. However, PCL libraries target very specific subsets of .NET and if you look at a PCL lib, you will see it explicitly references the
System.Runtime assembly. Since the XAML compiler is only pre-loading the references it reads from the project into the “reflection only”
AppDomain that explains not being able to find that exactly assembly by name for reflection purposes.
Ok, great, problem identified. So how can we fix it? Turns out it’s pretty simple:
- Unload your project file
- Open it in the XML editor
- Add an explicit reference to
System.Runtimeby hand. (I usually just go stick it right under the
Systemreference like so:
<Reference Include="System" /> <Reference Include="System.Runtime" />
Now we should just have to reload the project and we’re alllllll set. Err… nope. Didn’t work. WHAT!? “You lied to me!” Well, no, it turns out that after I fixed this it still didn’t work at first, so I debugged again and got another exception like above, but this time saying that it couldn’t find the assembly
System.Resources.ResourceManager. This assembly is again rolled up into
mscorlib in 4.5, so you need to play the same trick with a manual project reference. Therefore your final project file should look like so:
<Reference Include="System" /> <Reference Include="System.Runtime" /> <Reference Include="System.Resources.ResourceManager" />
Hopefully this work saves other people time and the cost of anger management therapy. I also hope it encourages Microsoft to address the issue with a simple update that has knowledge of PCLs and implicitly loads type forwarded assemblies into the “reflection only”
AppDomain so they are properly discovered.