Azure App Service Plans in which Azure App Services run include 1, 10, 50, or 250 GB of disk space, depending on the pricing tier you choose, but if you need more than that, or if you don’t want to pay the premium price just for extra disk space, you’re out of luck. I recently worked on a project upgrading a website that required hundreds of GB of files be stored in a virtual directory in the file system. When retrieved, some of the files referred to other files via a relative path. In addition, the files had to be protected by authentication and authorization. We had to maintain the hierarchy of the file system and integrate security so they could only be retrieved through the Web application, so we couldn’t just store the files in BLOB storage.
You can’t add drive space or additional drives to Azure Web Services.
We soon found out that the obvious solution, to create a large file share in Azure and attach it to the App Service Plan, isn’t possible. You can’t add drive space or additional drives to Azure App Services. We spent many hours searching for a solution and found that this is, in fact, a highly requested feature in Azure and that quite a lot of us would like to see a solution. At one point, we came across a new Azure feature, now in Preview, to do just what we needed, but soon found out that it’s only going to be available in Linux-based App Services. So close! Undaunted, we came up with a work-around.
The Work-Around
Our application is written in ASP.NET MVC hosted on IIS in Azure APP Services, but the principles can be applied to many Web platforms. To re-state the problem, the browser sometimes requests files from the server via a path to a virtual directory. Many of the files returned from the server contain relative paths to additional files and the files can’t be modified, so the relative paths must continue to work. The amount of disk space available to the App Services is far less than the amount we require. The goal is to make the server respond to the unmodified URLs as though it’s retrieving files from a virtual directory (a very large one).
The first hurdle we faced was preventing the Web server from attempting to find and retrieve the files because they won’t be on the local drive where the website resides. The fundamental purpose of Web servers, after all, is to retrieve requested HTML and other files and return them to the browser. What I needed to happen instead was for IIS to allow me to handle the request. As far as IIS (the underlying technology used to host App Services in Azure) goes, when it encounters file extensions at the end of a URL, it assumes that you want to retrieve a file (through a process called request filtering) and goes to the file system looking for it. This happens very early in the processing pipeline, long before the request gets to ASP.NET, let alone any code under my control. Luckily, IIS has a way to alter this behavior for a specific path. Because we had to modify how IIS works, the change had to be made in the web.config file, as shown in Figure 1.
The configuration change shows how to override IIS’s default request filtering. In our case, all of the files in question resided in a virtual directory named Media. With this configuration change, any URL path that consists of the domain, followed by /Media/, followed by anything, using any of the HTTP verbs listed (we only need the GET verb) will be passed to the TransferRequestHandler, which is the default handler for ASP.NET MVC. That means that when the browser asks for anything in this path, instead of looking for the file on disk and returning it, the server turns the request over to ASP.NET MVC, which looks at the route to determine what to do next.
In order to alleviate the need for an action parameter in the URLs for retrieving files and to allow for any number of subfolders, you need to create a special route. For example, a typical request for a file might look like this:
http://localhost:60234/media/somefolder/somefile.html
To accomplish this, add the following route before the default route in the RouteConfig.cs file, as shown in Figure 2.
The expression {*filePath} tells ASP.NET MVC to handle any number of additional parameters as a single parameter named filePath.
Next, create a MediaController to handle the requests going to the /Media/ path, as shown in Figure 3.
Now you can test the configuration by inserting a breakpoint in the controller method, running the Web app, and navigating to any file under the Media folder. For example:
http://localhost:60234/media/somefolder/somefile.html
When you hit the breakpoint using this URL, you’ll find that the filePath parameter holds the value somefolder/somefile.html. You can now use whatever code you like to come up with the contents of this file and return it to the browser. You could even create the file right then and there if you like, as in the example above. In our case, we chose to use a private Azure BLOB storage container to hold the actual files. The app logs into Azure BLOB storage, retrieves the bytes, and passes them back to the browser. For all the browser knows, there’s a Media folder on the server with a subfolder named SomeFolder that contains a file named somefile.html.
It's important to note that nothing has changed as far as the browser is concerned. The link works exactly as it did in the old system when there was a massive virtual directory to serve files from. As showing the code to retrieve files from Azure BLOB storage is pretty well covered in other places and because setting up a BLOB storage account in order to run the code would complicate the sample, in this article, I’m using files added to the solution as embedded resources as the source of the files I’m returning. Where you get the files is really up to you. I will say that Azure BLOB storage allows file names with forward slashes in them that can naturally mimic a hierarchical file system with folders, it recognizes and stores mime types, and it’s quite inexpensive and only takes a minute to set up in the Azure portal. So for most scenarios, that’s an excellent choice, but not the only choice.
The next bit is what makes this solution really interesting. In our case, the retrieved file often contained relative paths to additional files it expected to be in the same folder structure. For example, the sample HTML page might reference a JavaScript file named MyScript.js that it expects to find in a /JavaScript/ folder under SomeFolder. When the sample HTML page is loaded, the browser will try to load the following URL (or you can load the file directly by typing in this address):
http://localhost:60234/media/somefolder/
javascript/myscript.js
This request, because it’s also in the Media path, will be passed to the Media controller. The breakpoint we set earlier in the controller method will show that this time, the filePath variable will have the value SomeFolder/JavaScript/MyScript.js. Now we can retrieve that file and return it. In addition, because the browser is handling relative paths, the special folder .. is handled properly to refer to the parent folder. For example, if MyScript.js references the file ../../this is a sample file.pdf, the resulting URL requested by the browser will be "http://localhost:60234/media/this is a sample file.pdf" as expected.
Another nice feature of this approach is that because the requests are coming through an MVC Controller, you can use the standard [Authorize] attributes on the method to only allow calls from logged in users. You can also restrict access to files based on the user’s role or even roll your own security using the SecurityPrincipal information provided by ASP.NET. In our case, this allowed us to secure the files and prevent users who aren’t logged in from accessing them.
Not All Good News
As with every work around, there are some down sides. Processing files through the ASP.NET MVC pipeline will cost more in resources than simply allowing IIS to retrieve and stream the files back to the caller. This means that you’ll need more memory and processing power than you would when using a virtual directory. On balance, that’s not a bad tradeoff for the flexibility and control you’ll gain, but it’s something to be aware of, especially on a very high-volume site.
Summary
In this article, you saw how to override the default request filtering that IIS uses for a specified URL path, giving you control over how requests to that path are handled. You saw how to configure a route in MVC to allow paths to a depth of an arbitrary number of folders to be processed to mimic a hierarchical file structure. Finally, you saw how to write a ASP.NET MVC controller that allows you to come up with the file to be returned in any way you can imagine and return it to the browser without the browser knowing that the file wasn’t being served up by IIS from disk. Perhaps some day, Microsoft will allow us to control the amount of disk space we want our Azure App Services to have, but until then, there’s a workaround that’s not difficult to implement.