Handling Expires Headers in ASP.Net MVC - asp.net

I need advice or suggestions on how to add Expires Headers to my CSS, Image and JavaScript files in ASP.Net MVC.
A key issue is that the software is not in a single location. It is distributed to clients who handle the hosting so I would rather have a solution that doesn't require manual configration in IIS unless it's unavoidable!
I googled around and the majority of answers seem to be focused on content that is returned via a controller. Can't do that for JavaScript files though..

Which IIS Version are you using?
If by 'manual IIS configuration' you mean having to open the IIS manager console, IIS 7.5 (and I think 7 as well) allows you to add expires headers to static content using only the web.config:
<system.webServer>
<staticContent>
<clientCache cacheControlMode="UseMaxAge" cacheControlMaxAge="00:30:00" />
</staticContent>
</system.webServer>

You can do something like this by writing a custom handler for your javascript files. In your Web.Config file of your MVC project look for the httpHandlers section. Add something like the following line:
<add verb="GET" path="/YourScriptsFolder/*.js" type="Your.Project.Namespace.And.Custom.Handler, Your.Assembly.Name" validate="false" />
This will force all requests for js files in that folder through your custom handler which will look something like this:
class CustomHandler : IHttpHandler
{
#region IHttpHandler Members
public bool IsReusable
{
get { return true; }
}
public void ProcessRequest(HttpContext context)
{
// Set any headers you like here.
context.Response.Expires = 0;
context.Response.Cache.SetExpires(DateTime.Parse("3:00:00PM"));
context.Response.CacheControl="no-cache";
// Determine the script file being requested.
string path = context.Request.ServerVariables["PATH_INFO"];
// Prevent the user from requesting other types of files through this handler.
if(System.Text.RegularExpressions.RegEx.Match(path, #"/YourScriptsFolder/[^/\\\.]*\.js"))
context.Response.Write(System.IO.File.ReadAllText(path));
}
#endregion
}
I haven't tested this code so you might run into some issues but this is the basic idea. There are a plethora of examples on ASP.Net custom handlers throughout the web. Here's a good example:
http://www.developer.com/net/asp/article.php/3565541/Use-Custom-HTTP-Handlers-in-Your-ASPNET-Applications.htm

Another, less complicated option is to add a randomized or versioned query string to the end of the file path.
<link rel="stylesheet" type="text/css" href="somecssfile.css?version=1.0.0.3" />
When you change the version, the browser will get a new copy of the file.

Related

Troubleshooting custom VirtualPathProvider

I have just created a customized VirtualPathProvider for my ASP.NET web application. It basically maps all virtual files in "~/Storage" and subdirectories to a directory other than the solution's directory.
Code essentials
private bool IsPathVirtual(string virtualPath)
{
String checkPath = VirtualPathUtility.ToAppRelative(virtualPath);
return checkPath.StartsWith(VirtualRootPath, StringComparison.InvariantCultureIgnoreCase);
}
public override bool DirectoryExists(string virtualDir)
{
return IsPathVirtual(virtualDir) ? ((FileSystemVirtualDirectory)GetDirectory(virtualDir)).Exists() : Previous.DirectoryExists(virtualDir);
}
public override bool FileExists(string virtualPath)
{
return IsPathVirtual(virtualPath) ? ((FileSystemVirtualFile)GetFile(virtualPath)).Exists() : Previous.FileExists(virtualPath);
}
In my case VirtualRootPath = "~/Storage", but that it configurable.
The problem
In IIS Express, when I debug via Visual Studio, the two public methods, required to resolve a virtual path, are not always called.
Calling http://localhost:7749/Storage triggers breakpoints on both methods. A 404 error is returned and desired. This is a correct behaviour to me
Calling http://localhost:7749/Storage/ExistingFile.txt doesn't trigger debug, and a different 404 error is returned. This is not correct
The difference between the two 404 errors is that when I call for the directory, it's ASP.NET responding (Server error in application '/') but when I call for the file inside that directory, it's IIS 8.0 responding (HTTP Error 404.0 - Not Found).
The question
Why, even if I correctly registered the VirtualPathProvider in my HostingEnvironment, doesn't IIS 8.0 let ASP.NET pipeline handle the HTTP request so it could be resolved correctly?
The workaround
After reading VirtualPathProvider doesn't (quite) work in production on IIS 7.5 I realized it could be a Web.config problem. Judging from the other question, it looks like that IIS handles certain file extensions independently, no matter if ASP.NET maps those to a virtual resource, a controller, or anything else. So, since I was trying to read an XML file (and perhaps not a JPEG), IIS didn't bother ASP.NET.
The workaround has been putting this line in Web.config's system.webServer.handlers section:
<add name="AspNetStaticFileHandler-XML" path="*.xml" verb="*" type="System.Web.StaticFileHandler" />
But this workaround works only for XML files. How to make a permanent fix that works for all files under the storage directory?
You can specify the storage directory as part of the 'path' field in the config, as follows:
<add name="AspNetStaticFileHandler-Storage" path="Storage/*" verb="*" type="System.Web.StaticFileHandler" />

SignalR /signalr/hubs 404 Not Found

I am trying to deploy a SignalR site on IIS. Code all works fine in VS. But getting the 404 not found error trying to resolve signalr/hubs so far I have tried.
1) Changing the Script ref to:
script src="<%= ResolveUrl("~/signalr/hubs") %>" type="text/javascript"></script>
2) Modifying Web.Config to include :
<system.webServer>
<validation validateIntegratedModeConfiguration="false" />
<modules runAllManagedModulesForAllRequests="true">
</modules>
</system.webServer>
3) Changing the invoke requests on IIS for UrlMappingsModule.
4) added SignalR.Hosting.AspNet.dll to see if that would help anything.
Not sure what else to try or check, any help or point in the right direction?
The order of route registration matters. I had this exact problem and fixed it by ensuring my global.asax.cs looked like this:
public class MvcApplication : System.Web.HttpApplication
{
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
RouteTable.Routes.MapHubs();
WebApiConfig.Register(GlobalConfiguration.Configuration);
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
}
}
This was in a web site using SignalR, MVC and WebApi all together.
The reason of this 404 error is hubs are not mapped, previously it would have to be done as answered by SimonF. If you are using SignalR version 2 RouteTable.Routes.MapHubs(); is now obsolete. For mapping hubs you can create a startup class as below.
[assembly: OwinStartup(typeof(WebApplication1.Startup))]
namespace WebApplication1
{
public class Startup
{
public void Configuration(IAppBuilder app)
{
// Any connection or hub wire up and configuration should go here
app.MapSignalR();
}
}
}
referenace : http://www.asp.net/signalr/overview/releases/upgrading-signalr-1x-projects-to-20
Try adding a wildcard application map to your server to help map the unknown extension in the script URL "~/signalr/hubs"
I was able to fix the 404 on ~/signalr/hubs by changing the following appSetting in web.config to "true".
<add key="owin:AutomaticAppStartup" value="false" />
If you are working on webforms, Please take the following steps
In the webconfig:
<validation validateIntegratedModeConfiguration="false" />
<modules runAllManagedModulesForAllRequests="true"/>
In the page add reference to hub as
<script src="/signalr/signalr/hubs"></script>
instead of
<script src="/signalr/hubs"></script>
Make sure your site's AppPool targets the correct version of .NET Framework.
I might be a little late but hope it helps someone.
Make sure this code run on start.
Sub Application_Start(ByVal sender As Object, ByVal e As EventArgs)
RouteTable.Routes.MapHubs()
End Sub
Because I added a global.asax to my application and then a codebehind but put this in Global.asax file
<%# Application Language="VB" CodeBehind = "Global.asax.vb" %> /*This was wrong*/
So when i tried to run the application, the Application_Start in my global.asax did not initialize the hub. that's why it couldn't resolve signalr/hubs
fixed with this in my Global.asax
<%# Application Inherits="_Global" Language="VB" %>
and this in my Global.asax.vb:
Public Class _Global
Inherits System.Web.HttpApplication
Check web.config, on segment AppSettings add
<add key="owin:AutomaticAppStartup " value="false" />
on Key must be white space on the end of the name key, like #Randy H.
I had no issues with routing in MVC3, but did get the path wrong. I would suggest you look at the source and see where the script is actually pointing, make sure it is resolving the application directory ok. And make sure you can physcially open the file with the correct path with your browser. E.g.
<script src="/MyWebApp/signalr/hubs" type="text/javascript"></script>
Can you open the file from a browser (replacing it with the correct subdirectory)?
If not, the hub might not be set up correct and it might point you in the right direction. Fiddler it.
The syntax I used was:
<script src="#Url.Content("~/signalr/hubs")" type="text/javascript"></script>
It might be the difference between Url.Content and ResolveUrl
Similar problem I had on IIS version, I fixed it by restarting AppPool, restart web and its working now.
there are potentially many causes of this 404 - a few common ones can be found by
Hit the url in the browser /signalr/hubs - if there is an error, you will see the full error come back.
check for duplication of HubName attribute if you have base class
ensure you have the correct version referenced in all projects (as to avoid binding errors)
For me the solution was to reinstall all the packages and restore all the dependecies.
Open nuget powershell and use this command.
Update-Package -Reinstall
I had the same problem, it was an asp.net project using signalR, it worked properly before I published but when I hosted it in IIS, it didn't.
After I inspected I realized the address /signalr/hubs is not correct, let me explain more, just do the following steps:
Open up your web application in the browser (Using IIS), you see the interface you designed, yeah? now press ctrl+U or right click on the page an select View Page Source.
You will see multiple links starting with <script> tag at the top of the page, find something like <script src="/signalr/hubs"></script> now click on it, if you are taken to the page which involves "404 - File or directory not found."you have some mistakes on defining address, find the true address and change the address in the address bar to observe the true result
In my case I needed to add my project name at the start of the address, so I had to change the address from:
<script src="/signalr/hubs"></script>
to
<script src="/MoveShape/signalr/hubs"></script>
in which MoveShape was my project name, now after pressing ctrl+U in the browser and following previously told steps, you click on the link and this time you see the true result, the page show codes starting with:
/*!
* ASP.NET SignalR JavaScript Library v2.2.2
* http://signalr.net/
*
* Copyright (c) .NET Foundation. All rights reserved.
* Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
*
*/
yeah! that's it, it works properly, keep in mind that it had no problem when I was testing it using Visual studio but not after hosting in IIS, as others have recommended make a correct address by applying <%= ResolveUrl("~/") %> or other methods.
In my case, I lose some owin dependencies then I got the 404 NotFound error.
When I added following dependencies, I retrieve the proxy javascript file clearly from expecting URL. Like URL:1111/singlar/hubs
Microsoft.Owin
Microsoft.Owin.Core
Microsoft.Owin.Host.SystemWeb
Microsoft.Owin.Security
Owin
Hope the answer helps someone.
In my startup.cs I changed the order from
app.UseWebApi(configuration);
app.MapSignalR();
to
app.MapSignalR();
app.UseWebApi(configuration);
And that fixed the issue.
It was working fine on localhost but the problem was that I have a custom global message handler that I'm using to display custom 404 messages on prod.
WebApiConfig.cs
config.MessageHandlers.Add( new WebApiCustomMessageHandler() );
public class WebApiCustomMessageHandler : DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken )
{
HttpResponseMessage response = await base.SendAsync( request, cancellationToken );
//...
//... custom 404 handling
//...
return response;
}
}
I was getting 404 for all requests to .../signalr
Fixing the order in startup.cs fixed the 404 issue in the global message handler.
What worked for me was that in the App_Start I created a new item and selected the OWIN Startup class.
Then I modified it to look like this: Adding the app.MapSignalR();
public class Startup1
{
public void Configuration(IAppBuilder app)
{
app.MapSignalR();
}
}
With This in the HTML
<script src="~/signalr/hubs"></script>

Manually Enable Compression Using httpModule

I'm trying to enable gzip compression on a site on our work intranet. Unfortunately, I don't have access to IIS, so any changes I make have been through web.config.
The server is running IIS 6 and .NET 2.0.
I enabled compression by adding an httpmodule
public class EnableCompression : IHttpModule
{
public void Init(HttpApplication application)
{
application.BeginRequest +=
(new EventHandler(this.Application_BeginRequest));
}
private void Application_BeginRequest(Object source, EventArgs e)
{
HttpContext context = HttpContext.Current;
context.Response.Filter = new GZipStream(context.Response.Filter, CompressionMode.Compress);
HttpContext.Current.Response.AppendHeader("Content-encoding", "gzip");
HttpContext.Current.Response.Cache.VaryByHeaders["Accept-encoding"] = true;
}
}
I registered in the web.config...
<system.web>
<httpModules>
<add name="EnableCompression" type="EnableCompression"/>
</httpModules>
</system.web>
Well, the above worked fine, except javascript and css files do not get compressed. From what I have found, I would have to add .js and .css to the application mappings in IIS 6, but of course I can't do that.
Apparently this can be done via the web.config file, but I don't know how to do that.
How can I enable compression for .js and .css files?
In IIS6, static code is not processed by managed HttpModules; it requires a native ISAPI.
One trick you can use is to convert your *.js and *.css files into dynamic files. You do this by changing them to be *.aspx, and set the ContentType to the right MIME type. For example:
this.Response.ContentType = "application/x-javascript";
The only other trick is to set StyleSheetTheme="" in the Page directive in the markup file. Otherwise, the runtime will insist on a <head> section in the document. You can enable output caching to minimize the performance impact.
I wrote a blog post about this on the JS side, in case it helps (CSS is similar, just with a different MIME type): http://www.12titans.net/p/dynamic-javascript.aspx
Unfortunately, this requires changing the name of your JS and CSS files in your app, but if you want compression and don't have access to IIS, I don't think there's a way around that.
If you want to keep the *.js and *.css extensions, you can do so by adding a handler for them in your web.config. For example:
<compilation>
<buildProviders>
<add extension=".css" type="System.Web.Compilation.PageBuildProvider"/>
</buildProviders>
</compilation>
<httpHandlers>
<add path="*.css" verb="*" type="System.Web.UI.PageHandlerFactory"
validate="true"/>
</httpHandlers>
This helps from a naming perspective, but not performance; the files will still be dynamic -- they are basically *.aspx files, but with a different extension. It also doesn't work correctly with ASP.NET Themes, since pages in the Themes folder can't be dynamic, regardless of their extension.
Adding to #RickNZ's answer:
Be careful about converting to dynamic. IIS is much faster at serving static files than dynamic (kernel mode). Only consider that if your app is dominated by bandwidth concerns.
One other option is to look into CDNs for static content (content delivery networks). Azure, Amazon, Akamai and others offer services. That's a very fast geo-located friendly way of serving static files.

Custom VirtualPathProvider not being used in IIS6

I added the following lines to Application_Start method in global.asax:
var provider = new TestVirtualPathProvider();
HostingEnvironment.RegisterVirtualPathProvider(provider);
Yet the 'TestVirtualPathProvider' is never used when deploying this application in IIS6 (it does in the ASP.NET Development Server).
Edit: the default path provider has always done its job correctly and served (non-embedded) views correctly. The problem is simply that I want to use my own path provider to provide embedded views. So, initially, I already had the following wildcard mapping configured:
Any possible reasons why this does not work in IIS6?
Are there any other factors (handlers for example) wich might influence the used VirtualPathProvider?
UPDATE: the fact that you want to handle extension-less URL's is an important point that's not mentioned in the question. Please see this page for help in setting up MVC with IIS 6: http://haacked.com/archive/2008/11/26/asp.net-mvc-on-iis-6-walkthrough.aspx. This should cover your scenario as well.
Most likely the same issue that I answered in this thread: http://forums.asp.net/t/995633.aspx
Basically, add this in your web.config:
<httpHandlers>
<add path="*" verb="GET,HEAD,POST" type="System.Web.StaticFileHandler" validate="true" />
</httpHandlers>
That other thread has some details that explain why this is necessary.
For the combination Custom VPP + IIS6 + Precompiled site, we need to add the VPP from AppInitailize();
public static class AppStart
{
public static void AppInitialize()
{
// code to be executed automatically by the framework
}
}
See also:
http://sunali.com/2008/01/09/virtualpathprovider-in-precompiled-web-sites/
I believe that you need to use an ISAPI filter in IIS6 to intercept URLs without extensions. Problem is that ISAPI will need to be done in c/c++.
IIS6 is configured to allow only certain extensions to be processed by the ASP.net pipeline.
To findout how you can redirct requests check out the post by DocV.

Why my HttpHandler is ignored?

In an ASP.NET application, I need to do some changes on every CSS file sent.
So I created an HttpHandler (inside the app itself), added:
<add verb="*" path="*.css" type="MyWebsite.CssTestHandler,MyWebsite"/>
to Web.config in system.web/httpHandlers and modified the handler like this:
public void ProcessRequest(HttpContext context)
{
context.Response.Clear();
context.Response.Write("Hello World");
context.Response.End();
}
But CSS files are still just like they were before, so the handler is just ignored.
What I'm doing wrong?
You need to setup a wildcard map in IIS, see the following link:
http://www.microsoft.com/technet/prodtechnol/WindowsServer2003/Library/IIS/5c5ae5e0-f4f9-44b0-a743-f4c3a5ff68ec.mspx?mfr=true
This will cause the request for the CSS file to be served by ASP.NET rather than just IIS.
If the application serves very high traffic, consider setting this mapping for .css files only, or even better change the CSS data in the page rather than changing the file.
Check this page for instructions on all 3 cases of IIS version (6, 7 Classic pipeline and 7 Integrated pipeline):
http://learn.iis.net/page.aspx/508/wildcard-script-mapping-and-iis-7-integrated-pipeline/
According to it, in case of Integrated pipeline, you need to add the following config parameter:
runAllManagedModulesForAllRequests="True"
The App ignores your CSS files because IIS ignores CSS files.
It's not mapped to an executable in IIS.
alt text http://www.fastpics.net/sharepics/imih41904722.jpg
Try adding the .css extension and map it to the .NET dll.

Resources