Serving Static Files

Mar 25, 2013 at 12:29 AM
Edited Mar 25, 2013 at 12:29 AM
I was hoping we could start a discussion on the possibility of serving static files from a precompiled library. I'm currently doing this in my project, but getting it into the main project will take a few extra considerations.

Current Work Item: http://razorgenerator.codeplex.com/workitem/12

How I'm Doing It
I add the files (any kind) to a directory and change the build action to embedded. Once this is done the "path" for the file becomes:
{Complete Assembly Name}.{Path from Root}
So, if my assembly is Webchanix.Web.ControlPanel and my file is in ~/Content/Css/Normalize.css then the full path would be:
Webchanix.Web.ControlPanel.Content.Css.Normalize.css
I can then retrieve the file using an ActionResult within an accessible controller using the following code:
using (var stream = 
typeof({controller}).Assembly.GetManifestResourceStream(path))
{
     stream.CopyTo(Response.OutputStream);
}
     return Content(null, contentType);
Content type is set earlier in the ActionResult based on the file type. Also, I'm accessing the Assembly using the actual controller serving the file ({controller}), since it's the same assembly that has the files it's attempting to server it works in my project. For this to work for an external project it would have to reference the assembly with the actual files - haven't looked into the best way to do this.

So while this works for me, before I invest too much time into this, is this something that you (admins) all think could be pushed into the main project?

Is there another, better way that I should investigate?
Coordinator
Mar 26, 2013 at 5:43 AM
What are you doing to transform "~/Content/Css/Normalize.css" to something that calls back in to your controller?

The way I went about it trying to make this feature work was by "new"-ing UrlHelper.Content that would then return a modified Url if it found that the file was an embedded resource. Unfortunately this doesn't work with Layout files. The lookup and instantiation of Layout files are done by WebPages so the Razor engine doesn't have an opportunity to tweak the urls.

An alternative that Ebbo and I discussed today was having a global route that would handle all static files under the root mapped to the assembly. The problem in this case is that for it to work, you'd have to turn on Run All Managed Modules (http://www.iis.net/learn/get-started/introduction-to-iis/iis-modules-overview#Precondition). Usually turning this on for production sites is discouraged (since it degrades perf).
Mar 26, 2013 at 1:20 PM
I have a "resource" controller that I use to serve the files from the assembly. Below is pretty much the entire code. There's one action for each type I server (css, js, gif, etc.)
<link href="@Url.HttpRouteUrl("Resources", new {httproute="", action = "css", file = "A_Reset"})" rel="stylesheet" type="text/css" />
or
<link href="~/resources/css/normalize" type="text/css" rel="stylesheet"  />
I can't have an extension on the file because IIS6 will try to serve the actual file (but it doesn't exist).

Then:
public class ResourcesController : Controller
{
        public ActionResult Css(string file)
        {
            return GetResource("css", file);
        }
        private ActionResult GetResource(string type, string file)
        {
//removed some code for brevity
                using (var stream = GetResourceStream(type, file))
                {
                    stream.CopyTo(Response.OutputStream);
                }

                return Content(null, contentType);
        }
        private Stream GetResourceStream(string type, string file)
        {
            return typeof(ResourcesController).Assembly.GetManifestResourceStream(fileLocation + type + "." + file + "." + type);
        }
}
I see a few things to tweak right off the bat:
  1. Refactor so that one action per type is not needed
  2. Determine which assembly has the static files (since this would be looking in the RazorGenerator assembly as written)
  3. Cache/Minimize - I currently use Output caching for caching and minimize before I had add them to the project, maybe bundling or other method to do on build or runtime
Coordinator
Mar 26, 2013 at 4:59 PM
1) You can avoid having multiple action methods if you were to deduce the content type from the extension. We did something similar in the WebPages Administration.
The relevant pieces of code are: http://aspnetwebstack.codeplex.com/SourceControl/changeset/view/11ecf31e5406#src/System.Web.WebPages/ApplicationParts/ResourceHandler.cs
and
http://aspnetwebstack.codeplex.com/SourceControl/changeset/view/11ecf31e5406#src/System.Web.WebPages/MimeMapping.cs

2) You should be able to add a routine as part of the PreApplicationStart that does this (the same place the RazorEngine is created)

The only problem with this solution is that it means explicit changes to the code. You wouldn't be able to take an existing site and run it through this. If you're willing to make this code change, I'd say create an extension on the UrlHelper that would render these Urls for you.

@Url.ManifestResourceUrl("~/Content/Scripts/Foo.js")
that generates /Resource?file=%2FContent%2FScripts%2FFoo.js when it's running inside the RazorEngine and the regular file route otherwise
Mar 26, 2013 at 6:09 PM
1 -

The problem with the extension is that on IIS6 it tries to serve the file before it routes - so it never gets to the routing - I had to remove the extension and make it part of the URL. It worked fine in dev and II7, but not deployed to IIS6 hosting.

I don't think this is a big deal, what I meant was that I think the actions and routes should be refactored to use the type as a parameter instead of the action name. That way it would be one action and allow for any type to be served. I'll copy and use the examples - that works out nicely.

2 -

I'll research this and give it a try - this piece is essential for this to be a viable option.