While working with ASP.NET Framework we sometimes need to get the physical path to a folder on the filesystem. The most common way to do this is using Server.MapPath("~/relative-folder"). I've researched best practices around this several times just to forget the details a couple of months later so in this post I'll outline my findings are share some of my own best practices.
So when building a web app there is mainly two "contexts" in which I need to get file system information, in my actual application code and in some of my unit tests. Most of the time I strive to mock out IO from my unit tests but in some scenarios, I also need to perform actual testing with the filesystem to be more confident that my tests are not giving me false positives.
Physical file paths in web applications
The "goto" standard back in the days was to always use HttpContext.Current.Server.MapPath() which would "translate" a relative path into a full file system path. BUT. This object is request-bound, meaning that it will only exist inside the context of a web request. If we run inside a background job with something like Hangfire or Quartz this object will not exist. That's why I always recommend using HostingEnvironment.MapPath(path) that will work in both request-context and in background jobs.
I also wanted to know if and how these might differ from one another so I created this table to see how Server.MapPath() behaves.
Code | Returns |
HttpContext.Current.Server.MapPath("") | D:\Dev\TestApp |
HttpContext.Current.Server.MapPath("/") | D:\Dev\TestApp\ |
HttpContext.Current.Server.MapPath("~/") | D:\Dev\TestApp\ |
HttpContext.Current.Server.MapPath("App_Plugins") | D:\Dev\TestApp\App_Plugins |
HttpContext.Current.Server.MapPath("/App_Plugins") | D:\Dev\TestApp\App_Plugins |
HttpContext.Current.Server.MapPath("~/App_Plugins") | D:\Dev\TestApp\App_Plugins |
HttpContext.Current.Server.MapPath("App_Plugins/") | D:\Dev\TestApp\App_Plugins\ |
HttpContext.Current.Server.MapPath("/App_Plugins/") | D:\Dev\TestApp\App_Plugins\ |
HttpContext.Current.Server.MapPath("~/App_Plugins/") | D:\Dev\TestApp\App_Plugins\ |
Note that it does not matter if the relative path starts with "/", "~/", or just the folder name. Also, note that any trailing slash in the relative path will be reflected with a trailing slash in the file system path.
Doing the same thing with HostingEnvironment.MapPath() reveals some differences.
Code | Returns |
HostingEnvironment.MapPath("") | Throws exception |
HostingEnvironment.MapPath("/") | D:\Dev\TestApp\ |
HostingEnvironment.MapPath("~/") | D:\Dev\TestApp\ |
HostingEnvironment.MapPath("App_Plugins") | Throws exception |
HostingEnvironment.MapPath("/App_Plugins") | D:\Dev\TestApp\App_Plugins |
HostingEnvironment.MapPath("~/App_Plugins") | D:\Dev\TestApp\App_Plugins |
HostingEnvironment.MapPath("App_Plugins/") | Throws exception |
HostingEnvironment.MapPath("/App_Plugins/") | D:\Dev\TestApp\App_Plugins\ |
HostingEnvironment.MapPath("~/App_Plugins/") | D:\Dev\TestApp\App_Plugins\ |
Note here that the relative path must start with either "/" or "~/" otherwise, the method will throw.
Overall conclusions and recommendations
- Always use HostingEnvironment.MapPath().
- A folder path is indicated by the trailing slash, otherwise, it's a file path. Consider always using trailing slash for folders.
- Always make sure that the relative path passed to MapPath() starts with a slash.
- Be aware that the method will respect and include any trailing slash from the relative path into the physical path.
- Avoid using things like AppDomain.CurrentDomain.BaseDirectory and build paths based on this as any virtual directories configured in IIS will not be respected with this approach.
Physical Paths in unit tests
This one is a little harder as we want our unit tests to be "self-contained" and not be dependent on any magic path on the developer's filesystem or a build server. Inside a unit test or any .NET app, you can always find the path to the executing program with AppDomain.CurrentDomain.BaseDirectory, in the case of a unit test this would return something like d:\Dev\TestApp\My.UnitTest\Bin\Debug. One might be tempted to traverse the path with ..\..\ to get to the project root but this only works if the folders created have this exact nomenclature. I would argue that there is a better way:
Create a folder called "MockFileSystem" inside your test project, this will act as the "root" of your application similar to what you would get from HostingEnvironment.MapPath("/"). Inside this folder, we can replicate the relevant files and store them inside our test project. It's important that we set the "Build action" for each item to "Content" and choose the "Copy if newer" option. This way the folder structure and files will be copied to the application´s root folder.
Have your application code depend on an abstraction of the MapPath()-method, in my case, this is an interface like this:
internal interface IFileSystemHelper
{
string MapPath(string path);
}
The implementation inside the web project would look like this:
internal sealed class FileSystemHelper : IFileSystemHelper
{
public string MapPath(string path)
{
return HostingEnvironment.MapPath(path);
}
}
And in my unit test project:
internal class MockFileSystemHelper : IFileSystemHelper
{
public string MapPath(string path)
{
string baseDir = AppDomain.CurrentDomain.BaseDirectory + "MockFileSystem\\";
path = path.TrimStart('~').Replace("/", "\\").TrimStart('\\');
var full = Path.Combine(baseDir, path);
return full;
}
}
To avoid the "issue" with some relative folders having trailing slashes and some not we could have our implementations strip any trailing slash from the returned path to be sure that we always get a full path without any trailing slash. Something like this:
public string MapPath(string path)
{
return HostingEnvironment.MapPath(path).TrimEnd('\\');
}