What is the correct configuration to implement a custom virtual path provider in IIS 7.5? The following code works as expected when run from Visual Studio using the ASP.NET Development Server but does not load the image when run from IIS.
.NET 4.0 Project File
CustomVirtualPathProvider.zip - SkyDrive file
Web.config
<?xml version="1.0"?>
<configuration>
<system.web>
<compilation debug="true" targetFramework="4.0" />
</system.web>
<system.webServer>
<modules runAllManagedModulesForAllRequests="true"/>
</system.webServer>
</configuration>
Default.aspx
<%# Page Title="Home Page" Language="C#" AutoEventWireup="true" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Virtual Path Provider</title>
</head>
<body>
<img src="Box.png" />
</body>
</html>
Global.asax
public class Global : System.Web.HttpApplication
{
void Application_Start(object sender, EventArgs e)
{
System.Web.Hosting.HostingEnvironment.RegisterVirtualPathProvider(new WebApplication1.CustomVirtualPathProvider());
}
}
CustomVirtualFile.cs
public class CustomVirtualFile : System.Web.Hosting.VirtualFile
{
private string _VirtualPath;
public CustomVirtualFile(string virtualPath) : base(virtualPath)
{
_VirtualPath = virtualPath.Replace("/", string.Empty);
}
public override Stream Open()
{
string ImageFile =
System.IO.Path.Combine(HttpContext.Current.Request.PhysicalApplicationPath, #"Crazy\Image\Path", _VirtualPath);
return System.IO.File.Open(ImageFile, FileMode.Open, FileAccess.Read);
}
}
CustomVirtualPathProvider.cs
public class CustomVirtualPathProvider : System.Web.Hosting.VirtualPathProvider
{
Collection<string> ImageTypes;
public CustomVirtualPathProvider() : base()
{
ImageTypes = new Collection<string>();
ImageTypes.Add(".PNG");
ImageTypes.Add(".GIF");
}
public override bool FileExists(string virtualPath)
{
if (IsImage(virtualPath))
{
return true;
}
return base.FileExists(virtualPath);
}
public override System.Web.Hosting.VirtualFile GetFile(string virtualPath)
{
if (IsImage(virtualPath))
{
return new CustomVirtualFile(virtualPath);
}
return base.GetFile(virtualPath);
}
private bool IsImage(string file)
{
return ImageTypes.IndexOf(file.ToUpperInvariant().Substring(file.Length - 4, 4)) > -1;
}
}
Filesystem
\Crazy\Image\Path\Box.png
IIS Configuration
Default site with no configuration changes.
Here is what I found to "fix" my issue.
http://sunali.com/2008/01/09/virtualpathprovider-in-precompiled-web-sites/
In brief:
HostingEnviornment explicitly ignores Virtual Path Providers in precompiled sites. You can circumvent this limitation by using reflection to call an internal version that omits this check. Thus, instead of calling
HostingEnviornment.RegisterVirtualPathProvider(new EmbeddedViewVirtualPathProvider();
call this instead:
typeof(HostingEnvironment).GetMethod("RegisterVirtualPathProviderInternal",
BindingFlags.Static | BindingFlags.InvokeMethod | BindingFlags.NonPublic)
.Invoke(null, new object[] {new EmbeddedViewPathProvider()});
I had the same problem but Tom Clarkson led me on the right track, he's absolutely right in that you need additional configuration in order to make IIS serve the content provider by the virtual path provider. I found a solution here
Here is an example web.config-snippet that I think will work for you
<system.web>
<httpHandlers>
<add path="*.png" verb="*" type="System.Web.StaticFileHandler" validate="true" />
<add path="*.gif" verb="*" type="System.Web.StaticFileHandler" validate="true" />
</httpHandlers>
</system.web>
You could also register a "wildcard httphandler" under a special location (eg "/MyVirtualFiles"), which might be useful if your virtual path provider serves many different file types.
<location path="MyVirtualFiles">
<system.web>
<httpHandlers>
<add path="*" verb="*" type="System.Web.StaticFileHandler" validate="true" />
</httpHandlers>
</system.web>
</location>
When FileExists returns true, it is interpreted as "there is a file there, IIS can serve it without ASP.NET". To get the next step of actually downloading the file to go through your virtual path provider, you need to set IIS to use ASP.NET to serve all image files and add code in global.asax or an http handler that will make use of your virtual path provider.
Related
Is it possible to programmatically test whether the current user is able to access a given path? This would be a path to a resource somewhere that I need to restrict access to, and it would be a resource which is being accessed via a handler rather than directly by its path.
For example, given these configuration settings:
<authentication mode="Forms">
<forms loginUrl="~/login/" defaultUrl="~/private/" protection="All" cookieless="UseCookies" slidingExpiration="true" path="/" />
</authentication>
...
<location path="private">
<system.web>
<authorization>
<deny users="?" />
<allow users="*" />
</authorization>
</system.web>
</location>
<location path="private/general">
<system.web>
<authorization>
<allow roles="general" />
<deny users="*" />
</authorization>
</system.web>
</location>
Could we do something like this?:
HttpContext.Current.User.Identity.IsAllowedToAccess("~/private/general/my-resource")
Which would return true for users within the 'general' role, and false for anyone else?
Note that the configuration settings are just an example - there could potentially be a lot more location definitions and roles etc., so testing using lots of myPath.StartsWith("/private/") statements isn't really a good solution.
I never quite managed to figure this out, so I had to change the approach slightly.
This is assuming that the resource we're trying to get at is one that wouldn't normally be served by IIS - in my case, this is web forms user controls (.ascx), which I want to get via Ajax, rendered to a string, which can then be injected by JavaScript somewhere in the HTML.
I got rid of the handler, and instead made it so that any requests to these resources were made directly to the resource, but with a slightly modified .ascx.asmx extension. For example, /private/general/my-resource.ascx.asmx.
I then added a HTTP module (integrated pipeline mode):
<system.webServer>
<modules runAllManagedModulesForAllRequests="true">
<add name="MyResourceHttpModule" type="MyApp.Controls.MyResourceHttpModule" />
</modules>
</system.webServer>
(or classic pipeline mode):
<system.web>
<httpModules>
<add name="MyResourceHttpModule" type="MyApp.Controls.MyResourceHttpModule" />
</httpModules>
</system.web>
And the related code:
using System;
using System.Web;
namespace MyApp.Controls
{
public class MyResourceHttpModule : IHttpModule
{
#region IHttpModule Members
public void Dispose()
{
}
public void Init(HttpApplication context)
{
context.AuthorizeRequest += new EventHandler(OnAuthorizeRequest);
}
#endregion
void OnAuthorizeRequest(object sender, EventArgs e)
{
var application = (HttpApplication)sender;
if (application.Context.Request.Url.AbsolutePath.EndsWith(".ascx.asmx"))
{
application.Context.RewritePath("~/Services/MyResource.asmx/GetResource");
}
}
}
}
This module rewrites the requested URL to a web service (MyResource.asmx), which then strips off the extra extension we originally added, and finally serves the file we're trying to get to (in this case, a web forms user control):
using System.Collections.Generic;
using System.Web;
using System.Web.Script.Services;
using System.Web.Services;
using System.Web.UI;
namespace MyApp.Services
{
[ScriptService]
public class MyResource : System.Web.Services.WebService
{
[WebMethod(EnableSession = true)]
public object GetResource()
{
var controlPath = HttpContext.Current.Request.RawUrl;
if (Paths.ApplicationPath != "" && controlPath.StartsWith(Paths.ApplicationPath)) controlPath = '~' + controlPath.Substring(Paths.ApplicationPath.Length);
if (controlPath.EndsWith(".asmx")) controlPath = controlPath.Substring(0, controlPath.Length - 5);
using (var myUserControl = (MyApp.Controls.MyUserControl)(new Page()).LoadControl(controlPath))
{
myUserControl.DataBind();
return myUserControl.RenderToString();
}
}
}
}
(This uses this RenderToString extension method):
public static string RenderToString(this System.Web.UI.Control control)
{
var stringBuilder = new StringBuilder();
using (var stringWriter = new StringWriter(stringBuilder))
{
using (var htmlTextWriter = new System.Web.UI.HtmlTextWriter(stringWriter))
{
control.RenderControl(htmlTextWriter);
return stringBuilder.ToString();
}
}
}
This all works because forms authentication runs through the authorization rules set in web.config before allowing the request, so we know that by the time it reaches our HTTP module it's OK to serve the requested file because the current user must have permission to access the path.
When im trying to use my aspx file
#using PagedList.Mvc;
...
#Html.PagedListPager(ViewBag.test, page => Url.Action("Test", new { page })).
I'll get this error message
'System.Web.Mvc.HtmlHelper<dynamic>' has no applicable method named 'PagedListPager' but appears to have an extension method by that name. Extension methods cannot be dynamically dispatched. Consider casting the dynamic arguments or calling the extension method without the extension method syntax.
My .cs file the code look like this
using PagedList;
private List<Quality> Qualitys()
{
List<Quality> Qualitys = new List<Quality>()
{
new Quality(){ID=1,Name="All Qualitys"},
new Quality(){ID=2,Name="1"},
new Quality(){ID=3,Name="2"},
new Quality(){ID=4,Name="3"},
new Quality(){ID=5,Name="4"},
new Quality(){ID=6,Name="5"}
};
return Qualitys;
}
public ActionResult Test(int? page)
{
int pageSize = 3;
int pageNumber = page ?? 1;
ViewBag.test = Qualitys().ToPagedList(pageNumber, pageSize);
return View();
}
In my ~Views/Web.config file I'll have there.
<namespaces>
<add namespace="System.Web.Mvc" />
<add namespace="System.Web.Mvc.Ajax" />
<add namespace="System.Web.Mvc.Html" />
<add namespace="System.Web.Optimization"/>
<add namespace="System.Web.Routing" />
<add namespace="Web" />
</namespaces>
I cant see why Im getting this error, please help.
You need to cast dynamic properties (in this case ViewBag.test) to the required type. it should be
#Html.PagedListPager((IPagedList)ViewBag.test, page => Url.Action("Test", new { page }))
I have a web app that uses new WebSocket feature of ASP.NET 4.5. I have a custom handler that does following:
public class WebsocketHandler : IHttpHandler
{
public void ProcessRequest(HttpContext context)
{
if (context.IsWebSocketRequest)
{
context.AcceptWebSocketRequest(WebSocketRequest);
}
}
public bool IsReusable { get { return false; } }
private async Task WebSocketRequest(AspNetWebSocketContext context)
{
//Processing
}
}
This works well when I host my app on IIS 8, but when I run this app in a IIS hosted web core, IsWebSocketRequest property is always fasle.
So, the questin is: does IIS 8 hosted web core supports websockets and if it does, what I need to do to enable it?
Well, I've figured out that HWC does support WebSockets by the fact that IIS Express 8 does, and it is basically a wrapper over HWC. After that I've examined IIS Express applicationhost.config and found that there were a few things there that I've missed.
The complete list of the changes that I've made to my HWC applicationhost.config to enable Websockets support is following:
Add <section name="webSocket" overrideModeDefault="Deny" /> element to the <sectionGroup name="system.webServer"> of <configSections>
Add <webSocket /> element to <system.webServer>
Add <add name="WebSocketModule" image="%windir%\System32\inetsrv\iiswsock.dll" /> element to <globalModules>
Add <add name="WebSocketModule" lockItem="true" /> element to the <modules>
After that Websockets worked like a charm.
I am trying to use a http hadler, so I created a class lirary project and added this code:
namespace MyProject.Handlers
{
public class Class1 : IHttpHandler
{
public bool IsReusable
{
get { return false; }
}
public void ProcessRequest(HttpContext context)
{
context.Response.Write("Hello from custom handler.");
}
}
}
Then I compiled it and it generated the MyProject.Handlers.dll file. I put it in my asp.net bin folder and added this to the asp.net
<system.webServer>
<handlers>
<add name="TutorialHandler" verb="*" path="*" type="MyProject.Handlers.Class1, MyProject.Handlers" modules="IsapiModule" scriptProcessor="c:\Windows\Microsoft.NET\Framework\v4.0.30319\aspnet_isapi.dll" resourceType="File" />
</handlers>
<modules runAllManagedModulesForAllRequests="true">
//some stuff I *think* it doesn't make a difference
</modules>
//some stuff I *think* it doesn't make a difference
</system.webServer>
and
<system.web>
<httpHandlers>
<add verb="*" path="*" type="MyProject.Handlers.Class1, MyProject.Handlers" />
</httpHandlers>
</system.web>
But it just doesn't get called!
I start the debugger, set the breakpoint, but it doesn't stop. If I check the output html in firebug, the sentence "Hello from custom handler" is not there.
What am I doing wrong?
Thanks,
Oscar
EDIT: I'm using framework 4.0, IIS7.
Could it be that I am using routes? (not calling directly index.aspx, but /home or something like this?)
For some reason it wasn't being called when it was at the end of the handlers list. I moved to the top and got a strange error regarding webresources.axd. The reson was the path="*".
I couldn't find a matching path because I am using routes.
At the end I found out that I was using the wrong tool: the modules were the right thing for me, not handlers, because I would like to add things to my output depending on the case. So I changed my approach to use modules instead and it now looks good.
Thanks to all that helped me with comments!
How do I force IIS 7 to not cache images for a particular page?
I would have thought that it is your browser doing the caching.
In any case one way around this as long as your link is not statically declared in the html, is to append a random number on the end of the images url:
<img src="http://mywebsite/images/mypic.png?a=123456" />
the argument means nothing because you are doing nothing with it, but to the browser it looks like a new uncached link.
How you put that random number on the end is up to you:
<img src="javascript:getMyLink();" />
or from the code behind:
Image myImage = new Image();
myImage.Source = "myurl?a=" + Guid.NewGuid().ToString();
someOtherControl.Controls.Add(myImage);
(of course this is pseudo code, you need to check that the property names are correct).
In IIS7, you can do this either declaratively in your web.config, or programmatically.
<location path="YourPath">
<system.webServer>
<staticContent>
<clientCache cacheControlMode="DisableCache" />
</staticContent>
</system.webServer>
</location>
The programmatic solution requires a simple HttpModule that's registered to run for all requests in Integrated mode, where you look for the URLs that you're concerned about. Then call:
context.Response.Cache.SetCacheability(HttpCacheability.NoCache);
FWIW, you may want to consider disabling client-side caching only, while enabling server-side caching, by using HttpCacheability.ServerAndNoCache. Also, if you add a query string on the image names, you will prevent server-side caching by http.sys.
In case it helps, I cover techniques like these in detail in my book: Ultra-Fast ASP.NET.
I've had to deal with this a lot but i need to better understand your end goal as IIS7 will update it's cache if an image is changed on the server, so maybe what you are seeing is the browser cache, have you looked into etags?
The old fall back is to stick a random query string at the end of the image path which keeps the browser guessing.
One sure way to prevent it from caching is to make custom file handler for .gif, .jpg, .png extention (lookup iHttpHandler) (code below lifted from http://www.codeguru.com/csharp/csharp/cs_network/http/article.php/c12641/
using System.IO;
using System.Web;
using System.Globalization;
namespace MVPHacks
{
public class ImageHandler: IHttpHandler
{
public void ProcessRequest(System.Web.HttpContext ctx)
{
HttpRequest req = ctx.Request;
string path = req.PhysicalPath;
string extension = null;
string contentType = null;
extension = Path.GetExtension(path).ToLower();
switch (extension)
{
case ".gif":
contentType = "image/gif";
break;
case ".jpg":
contentType = "image/jpeg";
break;
case ".png":
contentType = "image/png";
break;
default:
throw new NotSupportedException("Unrecognized image type.");
} if (!File.Exists (path))
{
ctx.Response.Status = "Image not found";
ctx.Response.StatusCode = 404;
}
else
{
ctx.Response.StatusCode = 200;
ctx.Response.ContentType = contentType;
ctx.Response.WriteFile (path);
}
}
public bool IsReusable { get {return true; } }
}
}
And don't forget to remove the default image handlers and add your in both sections of the web.config
<httpHandlers>
<clear />
<add verb="*" path="*.jpg" type="MVPHacks.ImageHandler" />
<add verb="*" path="*.gif" type="MVPHacks.ImageHandler" />
<add verb="*" path="*.png" type="MVPHacks.ImageHandler" />
</httpHandlers>
<handlers>
<clear />
<add verb="*" path="*.png" type="MVPHacks.ImageHandler" name="png" />
<add verb="*" path="*.gif" type="MVPHacks.ImageHandler" name="gif" />
<add verb="*" path="*.jpg" type="MVPHacks.ImageHandler" name="jpg />
</handlers>