An Inside Look at ASP.NET 5 Execution – Part III – DNX Application Host

On the previous post of this series I followed code execution on the CLR Native Host and on the Managed Entry Point up to the point where the application layer is invoked. In this case, that layer starts on the application host provided by the runtime (Microsoft.Framework.ApplicationHost assembly).

If we take a look on the entry point (Program class) we can see , once again, the use of dependency injection (DI).

public Program(
    IAssemblyLoaderContainer container,
    IApplicationEnvironment environment,
    IServiceProvider serviceProvider
){ ... }

As discussed on the previous post of this series, instance Main methods are now supported. Note that the types that this class depends on were registered on the DI infrastructure by the Managed Entry Point.

The first task of the Application Host is to extract/consume some common options from the command line. This is where things such as the packages directory and run configurations are parsed (1) resulting in a DefaultHostOptions instance that will parameterize the remaining execution. In addition, the next command line option is assumed as a tentative application name and the remaining arguments are returned (2).

var app = new CommandLineApplication(throwOnUnexpectedArg: false);
//...
var optionPackages = app.Option("--packages ", "Directory containing packages", CommandOptionType.SingleValue); (1)
var optionConfiguration = app.Option("--configuration ", "The configuration to run under", CommandOptionType.SingleValue);
//...
defaultHostOptions = new DefaultHostOptions();
defaultHostOptions.PackageDirectory = optionPackages.Value();
defaultHostOptions.Configuration = optionConfiguration.Value() ?? _environment.Configuration ?? "Debug";
//...
var remainingArgs = new List();
remainingArgs.AddRange(app.RemainingArguments);
if (remainingArgs.Any())
{
  defaultHostOptions.ApplicationName = remainingArgs[0]; (2)
  outArgs = remainingArgs.Skip(1).ToArray();
}
//...

At this point, if we’re running “dnx . web” on the command line, the application name is “web” (which is actually a command on the project) and there aren’t any remaining arguments.

The major responsibility of the Application Host is to add support for the new project structure (project.json). Following the execution, we can see that this logic is based on the DefaultHost class (1). If a project is not found on the application root directory, an exception is thrown. On the other hand, if a project is found, DNX tries to expand the project command (“web” in this case) to find the final application name and arguments (2).

var host = new DefaultHost(options, _serviceProvider); (1)
// ...
var lookupCommand = string.IsNullOrEmpty(options.ApplicationName) ? "run" : options.ApplicationName;
string replacementCommand;
if (host.Project.Commands.TryGetValue(lookupCommand, out replacementCommand)) (2)
{
    var replacementArgs = CommandGrammar.Process(replacementCommand, ...);
    options.ApplicationName = replacementArgs.First(); (3)
    programArgs = replacementArgs.Skip(1).Concat(programArgs).ToArray();
}

At this point, for our example, the application name becomes “Microsoft.AspNet.Hosting” (3), since the “web” command is defined as follows on the project.json file:

"web": "Microsoft.AspNet.Hosting --server Microsoft.AspNet.Server.WebListener --server.urls http://localhost:5000"

The remaining arguments are the server type and endpoint which will be processed by that layer.

Part of the support for the new project system is the ability to load assemblies from project references and NuGet packages. To that end, new assembly loaders are added to the IAssemblyLoaderContainer. Note that this container was previously initialized by the Managed Entry Point; the Application Host simply adds support for other means of resolving assemblies.

host.AddLoaders(_container);

 

// Excerpt from AddLoaders 
var loaders = new[]
{
    typeof(ProjectAssemblyLoader),
    typeof(NuGetAssemblyLoader),
};
// ...
foreach (var loaderType in loaders)
{
    var loader = (IAssemblyLoader)ActivatorUtilities.CreateInstance(ServiceProvider, loaderType);
    container.AddLoader(loader) // ...
}

The last step is to invoke the entry point on the application assembly (the last layer). This is done by loading an assembly with the current application name and then using EntryPointExecutor, just like the Managed Entry Point did in order to invoke the Application Host.

 Assembly assembly = host.GetEntryPoint(options.ApplicationName);
 // ...
 return EntryPointExecutor.Execute(assembly, args, host.ServiceProvider);

At this point we finally get to ASP.NET code! I’ll continue from there on the following post.

As a final note: I didn’t go into the details of the DefaultHost class, which wraps the logic of handling the new project structure. Nevertheless, it worthy taking a look at the code, namely to the Initialize method and from there to the ApplicationHostContext class, which articulates the core logic of processing the project and resolving its dependencies. The ProjectResolver and Project classes are a must! 🙂

Advertisement

An Inside Look at ASP.NET 5 Execution – Part II – Native Host & Managed Entry Point

Side note before diving into the code: following the announcements from Build 2015 I’ve upgraded to VS 2015 RC and ASP.NET 5 and DNX beta 4 (the current stable version). This means I’ll be using the upgraded command line tools with their new names. Upgrading from the thins shown on the previous post is straightforward:

  • Install DNVM (previously KVM) using the command on Git Hub.
  • Change the sample project’s dependencies to beta 4 and update the framework designation on project.json. It now looks like this:
{
"webroot": "wwwroot",
"dependencies": {
"Microsoft.AspNet.StaticFiles": "1.0.0-beta4",
"Microsoft.AspNet.Mvc": "6.0.0-beta4",
"Microsoft.AspNet.Server.WebListener": "1.0.0-beta4",
"Microsoft.AspNet.Hosting": "1.0.0-beta4"
},
"commands": {
"web": "Microsoft.AspNet.Hosting --server Microsoft.AspNet.Server.WebListener --server.urls http://localhost:5000"
},
"frameworks": {
"dnxcore50": { }
},
}

  • Install DNX beta 4:
dnvm install 1.0.0-beta4 -arch x64 -r coreCLR -Persistent
  • Restore project dependencies using dnu (was kpm) on the project’s folder:
dnu restore
  • Start the application:
dnx . web
  • Pull the latest changes from the ASP.NET and DNX repos mentioned on the previous post and checkout the beta4 tag

On Visual Studio, I had to manually restore references for some projects but eventually everything loaded up. Finally, it’s worth checking the latest version of the DNX structure wiki page.

Now, into the code! Let’s start on the DNX itself. I’m not going to build or debug here but just by looking into the code we can get an idea of what’s going on. On the DNX source, there an “host” folder that contains the projects for the CLR Native Hosts and the Managed Entry Point (layers 1 and 2 on the DNX structure diagram).

Untitled

Accordingly to the documentation, the CLR Native Host (layer 1) is responsible for booting the CLR and calling the Managed Entry Point. The actions undertaken to boot the CLR depend on the version of the CLR. “For Core CLR the process involves loading coreclr.dll, configuring and starting the runtime, and creating the AppDomain that all managed code will run in”. The following excerpts were taken from the “dnx.coreclr\dnx.coreclr.cpp” file, and highlight the aforementioned steps.

HMODULE hCoreCLRModule = LoadCoreClr(); // (1)
// ...
pfnGetCLRRuntimeHost = (FnGetCLRRuntimeHost)::GetProcAddress(hCoreCLRModule, "GetCLRRuntimeHost");
// ...
hr = pfnGetCLRRuntimeHost(IID_ICLRRuntimeHost2, (IUnknown**)&pCLRRuntimeHost);
// ...
STARTUP_FLAGS dwStartupFlags = (STARTUP_FLAGS)(
STARTUP_FLAGS::STARTUP_LOADER_OPTIMIZATION_SINGLE_DOMAIN |
STARTUP_FLAGS::STARTUP_SINGLE_APPDOMAIN |
STARTUP_FLAGS::STARTUP_SERVER_GC
);
pCLRRuntimeHost->SetStartupFlags(dwStartupFlags);
// ...
hr = pCLRRuntimeHost->Start(); // (2)
// ...
hr = pCLRRuntimeHost->CreateAppDomainWithManager(
L"dnx.coreclr.managed",
//...
&domainId); // (3)

As previously described, the first thing to do is load the CoreCLR DLL (1). Then, the native host is created and started (2), allowing the creation of the AppDomain that will execute the managed code (3).

Having the app domain, DNX creates (4) and executes (5) a delegate for the managed entry point, which is implemented by the Execute method of dnx.coreclr.managed.DomainManager. When the managed code execution returns, the AppDomain is unloaded and the native host stopped (6).

LPCWSTR szAssemblyName = L"dnx.coreclr.managed, Version=0.1.0.0";
LPCWSTR szEntryPointTypeName = L"DomainManager";
LPCWSTR szMainMethodName = L"Execute";
HostMain pHostMain;

hr = pCLRRuntimeHost->CreateDelegate(
domainId,
szAssemblyName,
szEntryPointTypeName,
szMainMethodName,
(INT_PTR*)&pHostMain); // (4)

SetEnvironmentVariable(L"DNX_FRAMEWORK", L"dnxcore50");

// Call main
data->exitcode = pHostMain(data->argc, data->argv); // (5)

pCLRRuntimeHost->UnloadAppDomain(domainId, true); // (6)
pCLRRuntimeHost->Stop();

The managed execution (layer 2) starts on the DomainManager class which just packs the arguments and invokes the RuntimeBootstrapper class (dnx.host project). This class determines the assembly search paths and forwards execution to the Bootstrapper class. Here are the managed entry point responsibilities of “creating the LoaderContainer that will contain the required ILoaders(1), create an ILoader for DNX libraries (2) and “call the main entry point of the provided program(4).

// ...
var container = new LoaderContainer(); // (1)
// ...
var disposable = container.AddLoader(new PathBasedAssemblyLoader(accessor, _searchPaths)); // (2)
var name = args[0];
var programArgs = new string[args.Count - 1];
args.CopyTo(1, programArgs, 0, programArgs.Length);

var assembly = Assembly.Load(new AssemblyName(name));
// ...
var serviceProvider = new ServiceProvider();
serviceProvider.Add(typeof(IAssemblyLoaderContainer), container); // (3)
// ...
var task = EntryPointExecutor.Execute(assembly, programArgs, serviceProvider); // (4)

It’s worth pointing out that this is where the intrinsic dependency injection classes are used for the first time to register some core services (3). It’s also worth peaking into EntryPointExecutor which contains the logic to find a Main method on the provided assembly, with support for both static and non-static classes.

var programType = assembly.GetType("Program") ?? assembly.GetType(name + ".Program");
// ...
entryPoint = programType.GetTypeInfo().GetDeclaredMethods("Main").FirstOrDefault();
// ...
instance = programType.GetTypeInfo().IsAbstract ? null : ActivatorUtilities.CreateInstance(serviceProvider, programType);

Note that he “provided program” could be an application compiled to assemblies on disk and invoked directly by the bootstrapper. However, on our case this program is the generic application host provided by the runtime (Microsoft.Framework.ApplicationHost). I’ll continue from there on the following post!