ReBuildAll Blog
Thoughts (mostly) on .NET development

Serve MVC Views from a ZIP file   (MVC)   
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).



 

Comments

Re: Serve MVC Views from a ZIP file
This is a great article and I have used it in my applicaiton.
I have a slight issue though, maybe you can help me with it.

I have used your example to pull the views from the database. They are pulled in fine, but they have no layout associated with them. I have tried specifying a layout but it get an error "The view 'Guidelines' or its master was not found or no view engine supports the searched locations. The following locations were searched:".

How can I get it to just continue using the current layout?
Darren Mart Re: Serve MVC Views from a ZIP file
Great article, thank you. I'm still getting the hang of MVC3 and will hopefully leverage this article when I try to serve up partial views from a separate assembly.

My only suggestion is to tweak FileExistsInZip() so that it's not case-sensitive. The Views should really be found whether one goes to /TestZipped or /testzipped, took me a few minutes to realize why the latter wasn't returning the views.
Sergey Re: Serve MVC Views from a ZIP file
I see my problem.
My local IIS site - http://localhost/ZippedVirtualPathProvider
and that is why virtualPath looks like "~/ZippedVirtualPathProvider/Views/TestZipped/Complex.cshtml"
instead of "~/Views/TestZipped/Complex.cshtml"

Thanks for your response. Great article.

Lenard Gunda Re: Serve MVC Views from a ZIP file
@Sergey, have to look into that. I think it might be just a configuration thing. Which version of IIS are you trying to get this to work with?

@Marco Leo, I am not sure you can remap the location of the web.config file, especially since it contains vital information for IIS as well (if you are using IIS7 or later). While I have not confirmed this, I think that you cannot remap web.config. Have to do some digging to make sure.
Sergey Re: Serve MVC Views from a ZIP file
It seems that it doesn't work with IIS, however everything is fine with WebDev server. Do you have any thoughts what could be wrong? Thanks.
Marco Leo Re: Serve MVC Views from a ZIP file
hello @Lenardg, I found your post really interesting and I tried to implement a similar version but in my case I just wanted to Load views from another File system location outside the web-app, without zips.

Everything works almost fine :), I am having same problem with the web-config of the view. Now my application want to get the web-config in the virtual path under the web-app root instead of my Virtual path remapped root. And for same reason it can't resolve same css files too.

I hope you can help me and future people with the same problem.

Thank again for your post i found it really useful.

Marco Leo

Lenard Gunda Re: Serve MVC Views from a ZIP file
@Tom, glad you found my post useful. I think it possible to serve views from wherever you want them to be, be it ZIP files, databases or assemblies. Although assemblies might be tricky, because they are also loaded by ASP.NET, but before loading they are copied over to the temporary asp.net files folder. I myself have not tried your approach, so I do not know if there could be some sideeffects there.

But I think your problem might be in the override of GetCacheDependency(). My implementation checks if the file was served from the .ZIP file, and then it creates the cache dependency itself, specifying the .ZIP file as the object to monitor.

I think you should there specify the .DLL file (assembly) you pulled the view out of. From the error you are getting I think you are calling the base implementation, which tries to monitor your view and fails - of course, since the view itself does not exist on disk.
Tom Re: Serve MVC Views from a ZIP file
Thank you for your post. Your approach is exactly what I'm looking for, however, I would like the virtual views to reside in a different assembly. I have a "core" assembly that gets re-used across projects. I would love to be able to put re-usable views in the "core" assembly, then use your approach to pull the views when needed. I've tweaked your process to look for views in an assembly, but it fails. I keep getting the following error:

Directory 'C:\ProjectStore\xxx\xxx\xxx.Admin\Views\Person' does not exist. Failed to start monitoring file changes.

Is it possible to pull views from a different assembly? My goal is to have a single location for re-usable views.

Thanks agian for your post and insight.