Some time ago I wrote an article about
MVC and virtual views. I made a comment that you could create a
VirtualPathProvider that would serve content from a ZIP file. Some of you commented you would like to see an implementation for that. Well, here it is.
Please read that article for more information on how to implement a VirtualPathProvider. Here I will not go into details on everything involved.
You can download the entire example solution (Visual Studio 2010 and MVC 3 required!)
from here.
The example should run as soon as you load it up in VS2010. Try to request the following addresses (relative to the application root):
/Test and
Test/Complex for non zipped versions of views. And
/TestZipped and
/TestZipped/Complex for zipped versions of the views.
How to extract files from a .ZIP file?
For this example, I will use
SharpZipLib, which a freely available .zip file management library. It is already included in the example solution I linked at the beginning of this article. So if you downloaded that you will not need to download SharpZipLib separately.
Preparing to serve Views from a .ZIP file
I decided that the content I want to serve will be stored in a file called
Views.zip, that needs to be stored at the root of the application (
~/Views.zip). This .zip file has to have the same directory structure as the real MVC application. So we need a Views folder, and inside that, the controller folder. Inside the controller folder (or folders) there will be the actual views.
For the example solution I put the views for the "
TestZipped" controller into the
Views.zip file. There are two views,
Index.cshtml and
Complex.cshtml. Both include Razor markup to demonstrate that the solution demonstrated is actually fully functional. You could of course pack up the entire Views folder in the example solution, and it will still work
(I did try this, and it did work. If you do it, make sure you still leave web.config in place, as that is outside the scope of VirtualPathProvider. But you can just put everything else in the Views/ folder into the .ZIP file, and delete them from the actual folder. The application will still work!
)
The directory structure inside the .zip will make it easy to find and extract the files. Because the
VirtualPathManager receives virtual file names, like
/Views/TestZipped/Complex.cshtml, when we have the same structure in the .zip, it is easy to string compare the names.
Of course you could implement as complex a logic as you want to find the proper file
Creating a VirtualPathProvider for serving content from .ZIP files
The next step is to create a new
VirtualPathProvider. Please note that the implementation I provide is for demonstration purposes only, and might not be fully ready for production use.
public class ZippedVirtualPathProvider : VirtualPathProvider
{
public static void RegisterMe()
{
HostingEnvironment.RegisterVirtualPathProvider(new ZippedVirtualPathProvider());
}
I also created a little helper method to register our path provider.
We will override
FileExists(),
GetFile() and
GetCacheDependency(). Let's start with
FileExists().
We simply want to check if we have the .zip file is available and if the file can be found in the .zip file. If both conditions are true, we can indicate we found the file. Otherwise, we let the default implementation take over and perform any checks needed.
public override bool FileExists(string virtualPath)
{
if (IsViewsZipFound && FileExistsInZip(virtualPath))
{
return true;
}
else
{
return base.FileExists(virtualPath);
}
}
private bool IsViewsZipFound
{
get
{
if (File.Exists(ViewsZipPath))
{
return true;
}
else
{
return false;
}
}
}
private static string ViewsZipPath
{
get
{
return HttpContext.Current.Server.MapPath ( "~/Views.zip" );
}
}
private static string TransformVirtualPath2ZipPath(string virtualPath)
{
string fullPath = virtualPath;
if (fullPath.StartsWith("~/"))
{
fullPath = fullPath.Substring(2);
}
if (fullPath.StartsWith("/"))
{
fullPath = fullPath.Substring(1);
}
return fullPath;
}
private bool FileExistsInZip(string virtualPath)
{
string zipPath = TransformVirtualPath2ZipPath(virtualPath);
using (ZipInputStream s = new ZipInputStream(File.OpenRead(ViewsZipPath)))
{
ZipEntry theEntry;
while ((theEntry = s.GetNextEntry()) != null)
{
if (theEntry.IsFile && theEntry.Name == zipPath)
{
return true;
}
}
}
return false;
}
So to check if the .zip exists, we just use the
System.IO.File.Exists() method. To verify if the file exists, we read the .zip file entries and try to find one that matches our file. If we find it, we return
true. Simple enough.
For getting the file, we use similar code:
public override VirtualFile GetFile(string virtualPath)
{
if (IsViewsZipFound)
{
var file = ExtractFromZip(virtualPath);
if (file != null)
{
return file;
}
}
return base.GetFile(virtualPath);
}
private VirtualFile ExtractFromZip (string virtualPath)
{
string zipPath = TransformVirtualPath2ZipPath(virtualPath);
using (ZipInputStream s = new ZipInputStream(File.OpenRead(ViewsZipPath)))
{
ZipEntry theEntry;
while ((theEntry = s.GetNextEntry()) != null)
{
if (theEntry.IsFile && theEntry.Name == zipPath)
{
byte[] data = new byte[theEntry.Size];
s.Read(data, 0, data.Length);
return new ZippedVirtualFile(virtualPath, data);
}
}
}
return null;
}
Here we check if the .zip file exists, and then try to extract the file from it. If it is found, it is also returned. A new class called
ZippedVirtualFile is used to return the file, this will be introduced a little later below.
Finally, we override the
GetCacheDependency() method, because MVC uses that to monitor when it needs to rebuild/reread a file. What we will do is instead of returning a cache dependency on the file, we return a dependency on the .zip file. If the .zip file changes we will assume our file changes. Not very robust, but will work for this example.
public override System.Web.Caching.CacheDependency GetCacheDependency(string virtualPath, System.Collections.IEnumerable virtualPathDependencies, DateTime utcStart)
{
if (IsViewsZipFound && FileExistsInZip(virtualPath))
{
return new System.Web.Caching.CacheDependency(ViewsZipPath, utcStart);
}
else
{
return base.GetCacheDependency(virtualPath, virtualPathDependencies, utcStart);
}
}
ZippedVirtualFile
We need a little helper class that will hold our extracted file, and return a stream to it on demand.
public class ZippedVirtualFile : VirtualFile
{
public byte[] data;
public ZippedVirtualFile(string virtualPath, byte[] data)
: base(virtualPath)
{
this.data = data;
}
public override System.IO.Stream Open()
{
return new MemoryStream(data);
}
}
The class is very simple, it simply holds a byte array, and when a stream is requested, returns a new
MemoryStream that can be used to read the contents.
Using the ZippedVirtualPathProvider
As a final step, we need to register our provider. This can be done in
global.asax.cs by adding the following code to the
Application_Start() method:
protected void Application_Start()
{
// ...
ZippedVirtualPathProvider.RegisterMe();
}
And we are ready to go! (As soon as we create the Views.zip file of course).