9

Sitecore 8.2u5

Primary Question

Why does Optimizing Code cause Dependency Injection to fail?


We've used DI on many projects at this point and just had an issue with a recent one. The error presented itself like this:

No Parameterless constructor error

We quickly realized this is how the site would respond if DI wasn't working properly, as the controllers truly do not have a default constructor. After comparing past projects to this one, we found the following:

  • The app pools' where the problem manifested were originally running with 32-bit Mode = true
  • We toggled this to false in order for the iFilter to be read correctly - (see this question)
  • This immediately caused the YSOD above

We tracked the issue down to the “Optimize Code” checkbox in our primary web project (where DI is wired up):

Optimize checkbox in build tab

Workaround

By unchecking this box and rerunning a build, everything works fine in 64-bit mode. I verified in past projects that we have this unchecked as well.

I reviewed the two different DLLs using JustAssembly, but other than seeing the wildly different code (optimized vs non-optimized) nothing stood out.

I'm comfortable leaving this option unchecked, but obviously, it's there for a reason- to make the code more efficient. I was surprised to learn no one else has documented this issue.

DI is wired up as follows:

Configuration

<?xml version="1.0"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:set="http://www.sitecore.net/xmlconfig/set/" >
  <sitecore>
    <services>
      <configurator type="MyProject.Web.ServicesConfigurator, MyProject.Web" />
    </services>
  </sitecore>
</configuration>

Wired up using Kamsars method for adding to all controllers:

namespace MyProject.Web
{
    public class ServicesConfigurator : IServicesConfigurator
    {
        public void Configure(IServiceCollection serviceCollection)
        {
            serviceCollection.AddTransient<ISitecoreContext>(provider => new SitecoreContext());
            serviceCollection.AddTransient<ISitecoreService>(provider => new SitecoreService(Sitecore.Context.ContentDatabase));
            serviceCollection.AddTransient<IGlassHtml, GlassHtml>();
            serviceCollection.AddTransient<ISitecoreData, SitecoreData>();
            serviceCollection.AddTransient<IItemService, ItemService>();

            serviceCollection.AddScoped<IGlobalItems, GlobalItems>();
            serviceCollection.AddScoped<ILinkService, LinkService>();
            serviceCollection.AddScoped<ISearchService>(provider => new SearchService(Settings.GetSetting("Search.Index")));

            serviceCollection.AddSingleton<IPardotService, PardotService>();

            serviceCollection.AddMvcControllersInCurrentAssembly();
        }
    }
}

Again, this DI technique is working on multiple projects, but with the Opimize Code checkbox unchecked.

Why am I unable to run our application with optimized code?

  • Is this a question or an answer? Or both? – Pete Navarra Feb 9 '18 at 17:27
  • 1
    The question is why does optimizing code cause DI to fail. I will update to make more specific. – jrap Feb 9 '18 at 17:32
  • I think the answer lies buried in the combination of Release mode and Reflection used, particularly in AddMvcControllers[...]. I can't take a deep dive into this at the minute (4.30 AM) but I believe the explanation can be found here: blog.appliedis.com/2013/03/13/… – Mark Cassidy Feb 10 '18 at 3:29
  • The AddMvcControllers method exists in a separate project/DLL and works properly when optimized as it has the NoInlining decoration. I debugged locally and strangely when my Web project is optimized, the DLL that is passed to the AddMvcControllers method is Sitecore.Kernel which correctly finds zero Controllers to register. When the Web project is not optimized, the DLL passed to the method is the correct Web project, and therefore it finds Controllers to register. – jrap Feb 10 '18 at 15:20
  • I've found that a simple "fix" is to add the NoInlining decoration to the ServicesConfigurator, but that feels a bit odd as well. The ServicesConfigurator class is unchanged between optimized/non-optimized. Will continue to look a bit further for the real answer. – jrap Feb 10 '18 at 15:22
5

Thanks to @Jeroen and @Mark for helping to lead me in the right direction.

Debugging locally, I found that the Assembly crawled in the AddMvcControllersInCurrentAssembly changes depending on optimization. Code below:

public static void AddMvcControllersInCurrentAssembly(this IServiceCollection serviceCollection)
{
    AddMvcControllers(serviceCollection, Assembly.GetCallingAssembly());
}

The Assembly returned by GetCallingAssembly() is as follows:

  • Optimized: Sitecore.Kernel
  • Non-Optimized: MyProject (correct)

This is what causes the failure. The code can only register Controllers if the proper assembly is found. It appears the optimization process alters the order of operation slightly. GetCallingAssembly is listed below:

[SecuritySafeCritical]
[__DynamicallyInvokable]
[MethodImpl(MethodImplOptions.NoInlining)]
public static Assembly GetCallingAssembly()
{
  StackCrawlMark stackMark = StackCrawlMark.LookForMyCallersCaller;
  return (Assembly) RuntimeAssembly.GetExecutingAssembly(ref stackMark);
}

During the optimization process, the Stack ordering is changed in some way. I did not dig into this too far.

As it is evident that this method is error-prone, the fix is to pass in your assembly using one of Kamsar's supplied overloads:

serviceCollection.AddMvcControllers(typeof(ServicesConfigurator).Assembly);

This avoids using the Stack to find the calling assembly. I have confirmed that all assemblies in my solution are optimized and the application is working correctly.

| improve this answer | |
  • I don't think the stack ordering is changed, but the method calling AddMvcControllersInCurrentAssembly is inlined. Another, although perhaps worse, method to fix this is by adding the [MethodImplAttribute(MethodImplOptions.NoInlining)] attribute to each method calling AddMvcControllersInCurrentAssembly. – Jeroen Feb 23 '18 at 20:37
  • When I debugged the returned assembly differed between optimized/non-optimized, so something changes. And yes, the method you mentioned also works (I tried it), but not as elegantly. – jrap Feb 23 '18 at 20:40
  • Yes I agree. I have removed the AddMvcControllersInCurrentAssembly in my project since it depends on the calling method not being inlined which cannot be guaranteed. – Jeroen Feb 23 '18 at 21:28
1

I don't think serviceCollection.AddMvcControllersInCurrentAssembly(); works with Optimize code enabled. Can you try instead to register like this: serviceCollection.AddTransient(typeof(MyController));

Is the controller listed on this page /sitecore/admin/ShowServicesConfig.aspx

| improve this answer | |
  • The controller listed was the first one it tried to pull. None of the controllers are listed on ShowServicesConfig.aspx. I figured manually registering would work. I will look into it further. – jrap Feb 9 '18 at 20:17
  • I have a very similar setup and it works for me when i register the controller explicitly with serviceCollection.AddTransient(typeof(MyController)); but not with AddMvcControllersInCurrentAssembly extension method – Jeroen Feb 9 '18 at 20:21
  • thank you for the tip. It helped lead me in the right direction. I didn't want to go the manual route as it adds an additional step to development. The true fix was quite simple. – jrap Feb 10 '18 at 15:49

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.