Search code examples
c#asp.net-coreiisasp.net-core-3.0

Get the local path of a file in a virtual directory


I have an ASP.NET Core 3.0 MVC application with images in it. E.g.,

http://foo.bar/images/image.jpg

Now, the folder images is a virtual directory which is mapped to a network drive, such as \\192.168.1.1\images.

Question:

What method turns the information /images/image.jpg into \\192.168.1.1\images\image.jpg? I need to retrieve the physical path of the file from the relative web path.

In ASP.NET Web Forms, this could be done by something like Server.MapPath("~/images/image.jpg"), but this method doesn't exist in ASP.NET Core's HttpContext anymore.


Solution

  • As noted by @Akshay Gaonkar in the comments, Microsoft has explicitly evaluated and rejected this functionality in ASP.NET Core (reference):

    We don't have plans to implement this. The concepts don't really map in ASP.NET Core. URLs aren't inherently based on any directory structure. Each component has conventions that may map to directories, but this isn't something that can be generalized.

    And while a workaround is proposed using IFileProvider, it doesn't actually work for virtual directories. What you can do, however, is establish a mapping service for translating the base path—and optionally querying IIS to retrieve those mappings dynamically, as I’ll discuss below.

    Background

    This limitation stems from the fact that ASP.NET Core is no longer tied to IIS, but instead relies on an abstraction layer (e.g., IWebHostEnvironment) to talk to the web server; that is further complicated by the fact that the default ASP.NET Core Kestrel web server acts as a reverse proxy (reference):

    That's going to be rough. I don't think that's even possible for us to implement in the current reverse-proxy architecture. You're going to have to maintain a manual mapping table.

    Keep in mind that the concept of a virtual directory (or, even more so, a virtual application) is fairly specific to IIS as a web server.

    Workaround

    Unfortunately, as mentioned in the previous excerpt, your only real option is to create a mapping between your virtual directories and their physical locations, and then create a service that handles the translation for you.

    The following is a basic proof-of-concept for how you might accomplish that—though, of course, you'll probably want something more robust for production code.

    Interface

    This introduces an abstraction that can be used for dependency injection and testing purposes. I've stuck with MapPath() for consistency with the legacy Web Forms signature.

    public interface IVirtualFileProvider
    {
        string MapPath(string path);
    }
    

    Service

    The concrete implementation of the interface might pull the data from a configuration file, a database—or even the Microsoft Web Administration library. For this proof-of-concept, however, I'm just hard-coding them into the provider:

    public class VirtualFileProvider: IVirtualFileProvider
    {
    
        // Store dependencies
        private readonly string _webRootPath;
    
        // Map virtual directories
        private readonly Dictionary<string, string> _virtualDirectories = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) {
            { "Images", @"\\192.168.1.1\images" }
        };
    
        public VirtualFileProvider(string webRootPath) {
          _webRootPath = webRootPath;
        }
    
        public string MapPath(string path)
        {
    
            // Validate path
            if (String.IsNullOrEmpty(path) || !path.StartsWith("/", StringComparison.Ordinal)) {
                throw new ArgumentException($"The '{path}' should be root relative, and start with a '/'.");
            }
    
            // Translate path to UNC format
            path                = path.Replace("/", @"\", StringComparison.Ordinal);
    
            // Isolate first folder (or file)
            var firstFolder     = path.IndexOf(@"\", 1);
            if (firstFolder < 0)
            {
                firstFolder     = path.Length;
            }
    
            // Parse root directory from remainder of path
            var rootDirectory   = path.Substring(1, firstFolder-1);
            var relativePath    = path.Substring(firstFolder);
    
            // Return virtual directory
            if (_virtualDirectories.ContainsKey(rootDirectory))
            {
                return _virtualDirectories[rootDirectory] + relativePath;
            }
    
            // Return non-virtual directory
            return _webRootPath + @"\" + rootDirectory + relativePath;
    
        }
    
    }
    

    Registration

    The implementation requires knowledge of the default web root, for translating the path for files not in a virtual directory. This can be retrieved dynamically, as seen in @Pashyant Srivastava's answer, though I'm using the IWebHostEnvironment here. With that, you can register the VirtualFileProvider as a singleton life style with ASP.NET Core's dependency injection container:

    public class Startup 
    {
    
        private readonly IWebHostEnvironment _hostingEnvironment;
    
        public Startup(IWebHostEnvironment webHostEnvironment) 
        {
            _hostingEnvironment = webHostEnvironment;
        }
    
        public void ConfigureServices(IServiceCollection services)
        {
    
            // Add framework services.
            services.AddMvc();
    
            // Register virtual file provider
            services.AddSingleton<IVirtualFileProvider>(new VirtualFileProvider(_hostingEnvironment.WebRootPath));
    
        }
    
        public static void Configure(IApplicationBuilder app, IWebHostEnvironment env) 
        {
            …
        }
    
    }
    

    Implementation

    With your implementation registered, you can inject the provider into your MVC controller's constructor or even directly into your action:

    public IActionResult MyAction([FromServices] IVirtualFileProvider fileProvider, string file)
        => Content(fileProvider?.MapPath(file));
    

    Limitations

    The above code makes no effort to validate that the file actually exists—though that's easy to add via File.Exists(). That will obviously make the call a bit more expensive.

    Dynamic Mapping

    The above implementation relies on hard-coded values. As mentioned, though, the Microsoft Web Administration library offers methods for interacting with IIS programmatically. This includes the Application.VirtualDirectories property for pulling a list of virtual directories from IIS:

    var directories = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
    var manager     = new ServerManager();
    var site        = manager.Sites["Default Web Site"];
    var application = site[0]; 
                    
    foreach (var virtualDirectory in application.VirtualDirectories)
    {
        directories.Add(virtualDirectory.Path, virtualDirectory.PhysicalPath);
    }
    

    This can be integrated with the VirtualFileProvider to dynamically assess the available virtual directories if needed.

    Warning: The Microsoft Web Administration library hasn’t been updated to support .NET 5, and maintains dependencies on .NET Core 3.x libraries that are not forward compatible. It’s unclear when or if Microsoft will be releasing a .NET 5 compatible version. As your question is specific to .NET Core 3.1, this may not be an immediate concern. But as .NET 5 is the current version of .NET, introducing a dependency on the Microsoft Web Administration library may represent a long-term risk.

    Conclusion

    I know this isn't the approach you were hoping for. Depending on your exact implementation, however, this might be an acceptable workaround. Obviously, if this is a reusable library being placed on a variety of sites where you have no knowledge of the virtual directories, you'll need to separate the data from the implementation. This at least provides a basic structure to work off of, though.