Why does making Application_Start a static method (a recommendation of FxCopAnalyzer) cause 404 errors?

Viewed 721

Visual Studio recommended I install the new Microsoft.CodeAnalysis.FxCopAnalyzers in my project. It nicely identifies a number of improvements in the source, but one of its recommendations is:

CA1822: Member Application_Start does not access instance data and can be marked as static (Shared in `VisualBasic`).

I examined the routine and sure enough, FxCopAnalyzer was right. So I changed:

protected void Application_Start()

to

protected static void Application_Start()

However, when I make that change, I now get this:

HTTP Error 403.14 - Forbidden

The Web server is configured to not list the contents of this directory.

If I take out "static", it works again. But I'm a little confused why the framework cares if this method is static.

2 Answers

This was such an interesting question. It took me a while to understand the Asp.Net bootstrapping pipeline. I will not go into detail so much because it would take a lot so I will leave the details to OP.

Basically, Asp.Net framework dynamically creates an assembly and creates a dynamically created type which inherits your MvcApplication.

So here is the default MvcApplication

public class MvcApplication : HttpApplication {

    public static void Application_Start() {
        AreaRegistration.RegisterAllAreas();
        FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
        RouteConfig.RegisterRoutes(RouteTable.Routes);
        BundleConfig.RegisterBundles(BundleTable.Bundles);
    }
}

So what do I mean by Asp.Net framework dynamically creating another assembly and a type? You will notice that by checking the following code→ Let us modify Application_Start method:

public static void Application_Start() {
    var whatIsMyType =  GetType();
    //You will see that our actual type is of ASP.global_asax,
    //which inherits  MvcApplication, which inherits  HttpApplication      
    //Other Stuff...
}

Where does ASP.global_asax type is created? You will need to dig deep into source code but let me give you a hint

So how does the actual ASP.global_asax looks like?

[System.Runtime.CompilerServices.CompilerGlobalScopeAttribute()]
public class global_asax : global::<YourNameSpace>.MvcApplication {

    private static bool @__initialized;

    [System.Diagnostics.DebuggerNonUserCodeAttribute()]
    public global_asax() {
        if ((global::ASP.global_asax.@__initialized == false)) {
            global::ASP.global_asax.@__initialized = true;
        }
    }

    protected System.Web.Profile.DefaultProfile Profile {
        get {
            return ((System.Web.Profile.DefaultProfile)(this.Context.Profile));
        }
    }
}

Finally, we can move onto the actual answer: Why does making Application_Start static make the application behave in an unexpected way?

In HttpApplicationFactory class, which is used in the bootstrapping your application, there is the following line of code

methods = _theApplicationType.GetMethods(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static);
foreach (MethodInfo m in methods) {
    if (ReflectOnMethodInfoIfItLooksLikeEventHandler(m))
        handlers.Add(m);
}

The puzzle is solved at _theApplicationType. Please remember that this is of ASP.global_asax type. Your static method Application_Start is defined in MvcApplication type so reflection will not find it through _theApplicationType and as a result it will not be assigned to be called when application starts.

Here is a simple code to check this.

public class BaseClass {
    public static void StaticMethodInBaseClass() {
    }
}

public class DerivedClass {
    public void DerivedClassMethod() {
    }
}

//You will not get `StaticMethodInBaseClass` here
var methods = typeof(DerivedClass).GetMethods(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static);

Making Application_Start() static makes it not being part of the ASP.NET pipeline. Making the method static changes the method's signature and the framework is no longer able to find the method its looking for.

However...

I don't understand why it's not called, it should.

Here's are parts of HttpApplicationFactory that show that that it looks for BindingFlags.Instance and Binding.Static methods.

methods = _theApplicationType.GetMethods(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static);
foreach (MethodInfo m in methods) {
    if (ReflectOnMethodInfoIfItLooksLikeEventHandler(m))
        handlers.Add(m);
}

Then the method is invoked on HttpApplication object, since the method is now static the first param should be ignored and the static method should be called.

if (paramCount == 0) {
   method.Invoke(this, new Object[0]);
}

Why 404 / 403?

Application_Start is, by convention the place where routes are configured.

My toy app

I put together a toy up to eliminate anything obvious. The method is called.

using System;
using System.Reflection;
using System.Web;

namespace NETFrameworkConsoleApp2
{
    public class MyHttpApp : HttpApplication
    {
        protected static void Application_Start()
        {
            Console.WriteLine("Very important work");
        }
    }

    class Program
    {
        private MethodInfo _onStartMethod;        // Application_OnStart

        public static void Main()
        {
            //Flags from https://referencesource.microsoft.com/#System.Web/HttpApplicationFactory.cs,74e5273062f54e5f,references
            var methods = typeof(MyHttpApp).GetMethods(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static);

            var a = new MyHttpApp();

            var p = new Program();

            foreach (MethodInfo m in methods)
            {
                p.ReflectOnMethodInfoIfItLooksLikeEventHandler(m);
            }

            p._onStartMethod.Invoke(a, new Object[0]);
            Console.ReadLine();
        }


        private bool ReflectOnMethodInfoIfItLooksLikeEventHandler(MethodInfo m)
        {
            // From https://referencesource.microsoft.com/#System.Web/HttpApplicationFactory.cs,b0a90d9df37ace19,references
            if (m.ReturnType != typeof(void))
                return false;

            // has to have either no args or two args (object, eventargs)
            ParameterInfo[] parameters = m.GetParameters();

            switch (parameters.Length)
            {
                case 0:
                    // ok
                    break;
                case 2:
                    // param 0 must be object
                    if (parameters[0].ParameterType != typeof(System.Object))
                        return false;
                    // param 1 must be eventargs
                    if (parameters[1].ParameterType != typeof(System.EventArgs) &&
                        !parameters[1].ParameterType.IsSubclassOf(typeof(System.EventArgs)))
                        return false;
                    // ok
                    break;

                default:
                    return false;
            }

            // check the name (has to have _ not as first or last char)
            String name = m.Name;
            int j = name.IndexOf('_');
            if (j <= 0 || j > name.Length - 1)
                return false;

            // special pseudo-events
            if (StringUtil.EqualsIgnoreCase(name, "Application_OnStart") ||
                StringUtil.EqualsIgnoreCase(name, "Application_Start"))
            {
                _onStartMethod = m;
                //_onStartParamCount = parameters.Length;
            }
            else if (StringUtil.EqualsIgnoreCase(name, "Application_OnEnd") ||
                     StringUtil.EqualsIgnoreCase(name, "Application_End"))
            {
                //_onEndMethod = m;
                //_onEndParamCount = parameters.Length;
            }
            else if (StringUtil.EqualsIgnoreCase(name, "Session_OnEnd") ||
                     StringUtil.EqualsIgnoreCase(name, "Session_End"))
            {
                //_sessionOnEndMethod = m;
                //_sessionOnEndParamCount = parameters.Length;
            }

            return true;
        }

        internal static class StringUtil
        {
            //From https://referencesource.microsoft.com/#System.Web/Util/StringUtil.cs,d3a0b2a26cb3f1e1
            internal static bool EqualsIgnoreCase(string s1, string s2)
            {
                if (String.IsNullOrEmpty(s1) && String.IsNullOrEmpty(s2))
                {
                    return true;
                }
                if (String.IsNullOrEmpty(s1) || String.IsNullOrEmpty(s2))
                {
                    return false;
                }
                if (s2.Length != s1.Length)
                {
                    return false;
                }
                return 0 == string.Compare(s1, 0, s2, 0, s2.Length, StringComparison.OrdinalIgnoreCase);
            }
        }

        static Program() => Console.WriteLine(GetFrameworkName());

        static string GetFrameworkName()
            => ((System.Runtime.Versioning.TargetFrameworkAttribute)
                    (System.Reflection.Assembly.GetEntryAssembly()
                    .GetCustomAttributes(typeof(System.Runtime.Versioning.TargetFrameworkAttribute), true)[0]))
                    .FrameworkName; // Example: .NETCoreApp,Version=v3.0
    }
}
Related