Aspnetcore: In-Process hosting Directory.GetCurrentDirectory() location

Created on 22 Nov 2018  路  36Comments  路  Source: dotnet/aspnetcore

Hi folks. I was reading the documentation on the new IIS in-process hosting option and kind of disappointing about this point:

Directory.GetCurrentDirectory() returns the worker directory of the process started by IIS rather than the application directory (for example, C:\Windows\System32\inetsrv for w3wp.exe).

Source: https://docs.microsoft.com/en-us/aspnet/core/host-and-deploy/aspnet-core-module?view=aspnetcore-2.2

Any code that was expecting the location to point to the application folder will be up for a big surprise. This might also be a problem for libraries relying on the current behavior.

This difference is going to trip up developers trying to evaluate the new model. Even worse if they develop in one hosting model but deploy to another another hosting model.

The w3wp process path feels very useless in my mind. I can鈥檛 think of a use case where I would care about it.

area-servers servers-iis

Most helpful comment

We've added a helper to set current directory to correct value in 2.2 (https://github.com/aspnet/Docs/blob/master/aspnetcore/host-and-deploy/aspnet-core-module/samples_snapshot/2.x/CurrentDirectoryHelpers.cs) just call CurrentDirectoryHelpers.SetCurrentDirectory() as the first line of your Program.cs. We are also going to start setting current directory to correct value by default in 3.0.

All 36 comments

Sorry for not getting to this sooner, holidays in the states.

@epignosisx , appreciate the feedback. We are aware of this issue and attempted a fix (see: https://github.com/aspnet/IISIntegration/pull/1437), however making this fix is risky.

To make Directory.GetCurrentDirectory() work, we need to set the current working for the entire w3wp.exe process to your application directory. After changing the current directory, if IIS tries to load a module lazily or another IIS module relies on the current working directory being C:\Windows\System32\inetsrv, that will start failing, which may or may not be recoverable.

The way we host ANCM In-Process is similar to System.Web (inside of the w3wp process). System.Web had Directory.GetCurrentDirectory() as C:\Windows\System32\inetsrv too.

As a work around, we set the ContentRoot to be the application directory in Configuration. Anywhere that you can resolve IConfiguration, you can use IConfiguration[HostDefaults.ContentRootKey] to obtain the application directory.

If this issue affects many users, we may consider investigating more into resolutions.

cc @muratg @shirhatti

As @jkotalik alluded to, we will not revisit this unless it proves to be a pit of failure.

This is no different from dotnet run --project /path/to/project from a different directory OR no different from System.Web like Justin mentioned.

I definitely did not expect this to be such a hard issue to maintain interoperability with IIS. However, what would happen if as the first line of my Program.cs, this is done:

public class Program
{
    public static int Main(string[] args)
    {
        Directory.SetCurrentDirectory(...);
    }
}

Would this work in the context of my app without interfering with IIS? Because, if that鈥檚 the case, I鈥檓 fine with that and I鈥檓 guessing a lot of users will be fine with that too, that is, they do not care about deep integration with IOS other than the basic hosting.

Secondly, if I need to get real application directory, how can I do it when IConfiguration[HostDefaults.ContentRootKey] is not available? For example, we are following something similar to pattern to set up our logging:

https://github.com/serilog/serilog-aspnetcore/blob/a2c1d9c437f524b1488e27d40c1b789322b9889b/samples/SimpleWebSample/Program.cs#L1-L54

namespace SimpleWebSample
{
    public class Program
    {
        public static IConfiguration Configuration { get; } = new ConfigurationBuilder()
            .SetBasePath(Directory.GetCurrentDirectory())  // <----- How do we make this work?
            .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
            .AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production"}.json", optional: true)
            .AddEnvironmentVariables()
            .Build();


        public static int Main(string[] args)
        {


            Log.Logger = new LoggerConfiguration()
                .ReadFrom.Configuration(Configuration)
                .Enrich.FromLogContext()
                .WriteTo.Console()
                .CreateLogger();


            try
            {
                Log.Information("Getting the motors running...");


                BuildWebHost(args).Run();


                return 0;
            }
            catch (Exception ex)
            {
                Log.Fatal(ex, "Host terminated unexpectedly");
                return 1;
            }
            finally
            {
                Log.CloseAndFlush();
            }
        }


        public static IWebHost BuildWebHost(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
                   .UseStartup<Startup>()
                   .UseConfiguration(Configuration)
                   .UseSerilog()
                   .Build();
    }
}

Notice in the creation of the configuration this line: SetBasePath(Directory.GetCurrentDirectory()), how would this work with in-process hosting before any WebHost has been created?

We don't do static APIs like that anymore.

@tratcher how can the sample code above would work then?

Re-opening for @shirhatti to address.

  • We'll look into exposing ContentRoot via an environment variable into the 2.2.x Patch train.
  • For 3.0, we'll either have to expose it via a static 馃槩 or see if you we can get coreclr to allow us to set AppDomain.CurrentDomain.BaseDirectory ahead of time

The only workaround at the moment, is to Pinvoke directly and read the applicationPath yourself directly from iis config. See https://github.com/aspnet/AspNetCore/blob/master/src/IISIntegration/src/Microsoft.AspNetCore.Server.IIS/WebHostBuilderIISExtensions.cs#L35-L38

Does in-proc support passing parameters into Main via the web.config?

Nice to hear there are plans to expose an API or environment variable to enable this scenario. I鈥檓 fine with either one.

Does in-proc support passing parameters into Main via the web.config?

Nope

You can still define an environment variable in the web.config and read that in Main.

I would like my app to know where it is running without me having to add a config setting specifying where it is running.

Understood. We're discussing these as short term workarounds.

@epignosisx an easier workaround would be to set the CurrentDirectory to AppContext.BaseDirectory, ex:

c# public static void Main(string args[]) { CurrentDirectory.SetCurrentDirectory(AppContext.BaseDirectory); }

Can you try that and see if that works?

Can we please ensure this behavior change with in-process hosting is documented. I can see this tripping up a lot of folks.

It is documented and we are considering changing the default in 3.0: https://github.com/aspnet/AspNetCore/pull/4369

@jkotalik
For ASP.NET Core 2.2 just released, what is the solution for InProcess?
I tried proposed solution AppContext.BaseDirectory but it does not work - basically it fails at reading appsettings.json because it looks for the file here: c:\windows\system32\inetsrvappsettings.json

This is my Program.cs

using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using System;

namespace XYZ
{
    public static class Program
    {
        public static IConfiguration Configuration { get; } = new ConfigurationBuilder()
           .SetBasePath(AppContext.BaseDirectory)
           .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
           .AddEnvironmentVariables()
           .Build();

        public static void Main(string[] args)
        {
            CreateWebHostBuilder(args).Build().Run();
        }

        public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
           WebHost.CreateDefaultBuilder(args)
               .UseStartup<Startup>()
               .UseConfiguration(Configuration)
            ;
    }
}

This has another side effect I don't see mentioned yet. If debugging from Visual Studio, AppContext.BaseDirectory is the $(ProjectDir)/bin/netcoreapp2.2 folder, not $(ProjectDir) like it used to be when you used Directory.GetCurrentDirectory(). This also means things relative paths to wwwroot don't work without guessing if you need to move up directories.

Can you check what the value of AppContext.BaseDirectory is? Can try is to call Directory.SetCurrentDirectory(AppContext.BaseDirectory); at the beginning of Program.Main().

@jkotalik yes, value is C:Code{Solution}{Project}binDebug\netcoreapp2.2\

.UseContentRoot() gets set off of that value which means when I go to try and read my webpack manifest that lives at C:Code{Solution}{Project}\wwwroot\manifest.json, I get an exception because it is looking here C:Code{Solution}{Project}binDebug\netcoreapp2.2\wwwroot\manifest.json

@jkotalik, neither Directory.GetCurrentDirectory() nor AppContext.BaseDirectory yields productive results for us in ASP.NET Core 2.2 with in-process.

This is a mess for us and a breaking change.

AppContext.BaseDirectory does not work in debug mode for us; we have a method that reads from appsettings.json in Startup.cs but appsettings.json is not copied into the binDebug\netcoreapp2.2 directory.

To reproduce

  1. Run a project in debug mode
  2. VS creates the binDebug\netcoreapp2.2 directory but does not copy appsettings.json there
  3. We can't reach that file unless we hardcode the file location. Next, this gets even worse on production servers with docker images unless we do a bunch of quirks.
var builder = new ConfigurationBuilder()
    .SetBasePath(Directory.GetCurrentDirectory()) // will look for appsettings.json here c:\windows\system32\inetsrv\appsettings.json = project fails
    .SetBasePath(AppContext.BaseDirectory) // look in \bin\Debug\netcoreapp2.2 folder but file is not there, so project fails
    .SetBasePath(HostDefaults.ContentRootKey) // just produces "contentRoot" string
    .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);

IConfigurationRoot configuration = builder.Build();

We can't really upgrade to 2.2 at the moment.
Directory.GetCurrentDirectory() used to work fine in ASP.NET Core 2.1-2.1.5

Is there another method we can use to get the right path no matter if we are in debug mode, production mode, inprocess/outofprocess, server or local machine?

We ran into this plus a few others. This upgrade wasn't as smooth. I know this isn't a great solution but we have specific json files we load on startup. We used AppContext.BaseDirectory then have a helper method that recursively looks at the parent directory until we find the file we need. Then use that path. It worked for us but not sure about your setup. Not a great solution but once a fix is made we will remove this code.

@shapeh you always have the option of using OutOfProcess. We intentionally made InProcess opt-in for existing applications so we could work out some of these issues before moving apps.

@Tratcher - I see. We were/are just eager to go with InProcess because of the announced 4x performance gains in 2.2.

I found a work-around using IHostingEnvironment via DI and then doing something like:

.SetBasePath(hostingEnvironment.ContentRootPath)

ContentRootPath seems to be getting the value I want but I need to do additional testing.

This actually breaks all scenarios requiring pre building configuration before creating WebHost. For us this breaks because we wire up Serilog logging based on configuration in Program.cs. Same goes for Azure Key Vault configuration, which usually requires pre building configuration in order to read connection details.

Based on what @shirhatti suggested we used native method approach to extract IIS application path with a helper class and it seems to solve the problem for now.

Gist of helper class: https://gist.github.com/molesinski/aa8433f61bd20141e443d4fed4b33753

Then we just use:
.SetBasePath(IISHelper.GetContentRoot() ?? Directory.GetCurrentDirectory())

Using just SetBasePath for configuration and UseContentRoot for the WebBuilder helps with startup, but any time you make a relative call to File.Read or other IO classes, it is going to go back to looking in the netcoreapp directory.

For local debugging, if I combine @molesinski suggestion and @jkotalik suggestion and put this line first thing in Program.Main, I get the same behavior as 2.1 OutOfProcess:

Directory.SetCurrentDirectory(IISHelper.GetContentRoot() ?? AppContext.BaseDirectory);

Need to try some tests today and see if that causes any weird side effects in a deployed environment, but now I can at least reasonably debug other issues we may run into with 2.2 and InProcess mode.

@danjohnso you should always assume the current application directory comes from IHostingEnvironment.ContentRootPath that you should inject into services that perform IO related operations like File.Read. This is what .SetBasePath(...) sets. For InProcess ANCM Directory.GetCurrentDirectory() will be defaulting to IIS folder.

Instead of calling SetCurrentDirectory, I was injecting ContentRoot as:

new WebHostBuilder()
        .UseKestrel()
    .UseIIS()
    .UseIISIntegration()
    .UseContentRoot(IISHelper.GetContentRoot() ?? AppContext.BaseDirectory)

While that worked for somethings, calls like this would still look in AppContext.BaseDirectory (inetsrv):

File.OpenRead("wwwroot/dist/manifest.json")

I think the only way to get the old behavior back right now is to call SetDirectory up front and continue using GetCurrentDirectory for setting configuration base path and the content root.

Used @danjohnso solution above, early tests are promising.

It seems to me that reading local files for configurations that can be updated via CD tooling is a pretty common use case.

We've added a helper to set current directory to correct value in 2.2 (https://github.com/aspnet/Docs/blob/master/aspnetcore/host-and-deploy/aspnet-core-module/samples_snapshot/2.x/CurrentDirectoryHelpers.cs) just call CurrentDirectoryHelpers.SetCurrentDirectory() as the first line of your Program.cs. We are also going to start setting current directory to correct value by default in 3.0.

I have just spent a long time trying to identify why my application wasn't running in Azure.
I deployed a second application into a new directory in my App Service and it was failing. Presumably because the application in the new folder was resolving to the default application folder in the app service.
The solution is to update to use out of process and redeploy.
I don't know if this is documented somewhere, but it has taken me a long time to find this issue and join the dots.

See the "Availability in Azure App Service" here: https://blogs.msdn.microsoft.com/webdev/2018/12/04/asp-net-core-2-2-available-today/. ANCMV2 (which is required for In-Process) is still rolling out across Azure, so for now you do need to continue using Out-Of-Process.

@shapeh saved my day, thanks.
IHostingEnvironment via DI

        public Startup(IConfiguration configuration, IHostingEnvironment hostingEnvironment)
        {
            Configuration = configuration;
            this.hostingEnvironment = hostingEnvironment;
            Init();
        }
        void Init()
        {
            var dataDirConfig = $"{hostingEnvironment.ContentRootPath}/../data"; 
            dataDir = Path.GetFullPath(dataDirConfig);
            //read my data files from dataDir ...
        }

I solved with DI:
public Startup(IConfiguration configuration, IHostingEnvironment hostingEnvironment) { Environment.CurrentDirectory = env.ContentRootPath; ... }

Was this page helpful?
0 / 5 - 0 ratings