IIS hosted web core and websockets - asp.net

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.

Related

Programmatically test access to a given path with FormsAuthentication

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.

With Glimpse How To Turn It Off Without Disabling?

I love Glimpse but only when I' interested in what it has to tell me. I have a glimpse role that I can turn on and off to make glimpse go away (see code below) but what I really want is to be able to turn it on and off while it is enabled in my global.asax. I've tried going to site.com/glimpse.axd and set "turn glimpse off" but then on the next page refresh it is back.
What am I missing?
public class GlimpseSecurityPolicy : IRuntimePolicy
{
public RuntimePolicy Execute(IRuntimePolicyContext policyContext)
{
var httpContext = policyContext.GetHttpContext();
if (!httpContext.User.IsInRole("GlimpseUser"))
{
return RuntimePolicy.Off;
}
return RuntimePolicy.On;
}
public RuntimeEvent ExecuteOn
{
get { return RuntimeEvent.EndRequest; }
}
}
In My Web.Config:
<glimpse defaultRuntimePolicy="On" endpointBaseUri="~/Glimpse.axd">
<runtimePolicies>
<ignoredTypes>
<add type="Glimpse.AspNet.Policy.LocalPolicy, Glimpse.AspNet" />
<add type="Glimpse.Core.Policy.ControlCookiePolicy, Glimpse.Core" />
</ignoredTypes>
</runtimePolicies>
Ok, the reason why clicking on the "Turn Glimpse Off" button has no effect is because the ControlCookiePolicy is disabled in the config, hence clicking that button will have no effect.
So you need to remove that entry from the config to make that work again:
<add type="Glimpse.Core.Policy.ControlCookiePolicy, Glimpse.Core" />
when you are saying that
The other code public class GlimpseSecurityPolicy" is in my global.asax
you mean that the GlimpseSecurityPolicy is basically defined as an inner class of the Mvc Application class?
Either way if you would enable logging for Glimpse in the config
<glimpse defaultRuntimePolicy="On" endpointBaseUri="~/Glimpse.axd">
<logging level="Trace" />
<runtimePolicies>
<ignoredTypes>
<add type="Glimpse.AspNet.Policy.LocalPolicy, Glimpse.AspNet" />
</ignoredTypes>
</runtimePolicies>
</glimpse>
then you should see a glimpse.log file appear in the root of your web application, and once the application is started, you should see an entry like this:
2014-06-13 09:48:25.8498 | DEBUG | Discovered IRuntimePolicy of type 'SOME NAMESPACE+GlimpseSecurityPolicy' and added it to collection. |
If that is the case then the policy is actually discovered.
You can put a breakpoint inside the Execute method to check whether a call is actually made and what the outcome is.

Why is my HttpHandler not being fired?

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!

ASP.NET Bundles how to disable minification

I have debug="true" in both my web.config(s), and I just don't want my bundles minified, but nothing I do seems to disable it. I've tried enableoptimisations=false, here is my code:
//Javascript
bundles.Add(new ScriptBundle("~/bundles/MainJS")
.Include("~/Scripts/regular/lib/mvc/jquery.validate.unobtrusive.js*")
.Include("~/Scripts/regular/lib/mvc/jquery.validate*")
.Include("~/Scripts/regular/lib/bootstrap.js")
.IncludeDirectory("~/Scripts/regular/modules", "*.js", true)
.IncludeDirectory("~/Scripts/regular/pages", "*.js", true)
.IncludeDirectory("~/Scripts/regular/misc", "*.js", true));
//CSS
bundles.Add(new StyleBundle("~/bundles/MainCSS")
.Include("~/Content/css/regular/lib/bootstrap.css*")
.IncludeDirectory("~/Content/css/regular/modules", "*.css", true)
.IncludeDirectory("~/Content/css/regular/pages", "*.css", true))
Conditional compilation directives are your friend:
#if DEBUG
var jsBundle = new Bundle("~/Scripts/js");
#else
var jsBundle = new ScriptBundle("~/Scripts/js");
#endif
If you have debug="true" in web.config and are using Scripts/Styles.Render to reference the bundles in your pages, that should turn off both bundling and minification. BundleTable.EnableOptimizations = false will always turn off both bundling and minification as well (irrespective of the debug true/false flag).
Are you perhaps not using the Scripts/Styles.Render helpers? If you are directly rendering references to the bundle via BundleTable.Bundles.ResolveBundleUrl() you will always get the minified/bundled content.
To disable bundling and minification just put this your .aspx file
(this will disable optimization even if debug=true in web.config)
vb.net:
System.Web.Optimization.BundleTable.EnableOptimizations = false
c#.net
System.Web.Optimization.BundleTable.EnableOptimizations = false;
If you put EnableOptimizations = true this will bundle and minify even if debug=true in web.config
You can turn off minification in your bundles simply by Clearing your transforms.
var scriptBundle = new ScriptBundle("~/bundles/scriptBundle");
...
scriptBundle.Transforms.Clear();
I personally found this useful when wanting to bundle all my scripts in a single file but needed readability during debugging phases.
I tried a lot of these suggestions but noting seemed to work. I've wasted quite a few hours only to found out that this was my mistake:
#Scripts.Render("/bundles/foundation")
It always have me minified and bundled javascript, no matter what I tried. Instead, I should have used this:
#Scripts.Render("~/bundles/foundation")
The extra '~' did it. I've even removed it again in only one instance to see if that was really it. It was... hopefully I can save at least one person the hours I wasted on this.
Combine several answers, this works for me in ASP.NET MVC 4.
bundles.Add(new ScriptBundle("~/Scripts/Common/js")
.Include("~/Scripts/jquery-1.8.3.js")
.Include("~/Scripts/zizhujy.com.js")
.Include("~/Scripts/Globalize.js")
.Include("~/Scripts/common.js")
.Include("~/Scripts/requireLite/requireLite.js"));
bundles.Add(new StyleBundle("~/Content/appLayoutStyles")
.Include("~/Content/AppLayout.css"));
bundles.Add(new StyleBundle("~/Content/css/App/FunGrapherStyles")
.Include("~/Content/css/Apps/FunGrapher.css")
.Include("~/Content/css/tables.css"));
#if DEBUG
foreach (var bundle in BundleTable.Bundles)
{
bundle.Transforms.Clear();
}
#endif
There is also some simple way to control minification (and other features) manually. It's new CssMinify() transformer using, like this:
// this is in case when BundleTable.EnableOptimizations = false;
var myBundle = new StyleBundle("~/Content/themes/base/css")
.Include("~/Content/themes/base/jquery.ui.core.css" /* , ... and so on */);
myBundle.Transforms.Add(new CssMinify());
bundles.Add(myBundle);
// or you can remove that transformer in opposite situation
myBundle.Transforms.Clear();
That's convenient when you want to have some bundles special part only to be minified. Let's say, you are using some standard (jQuery) styles, which are getting under your feet (taking lots of excessive browser requests to them), but you want to keep unminified your own stylesheet. (The same - with javascript).
I combined a few answers given by others in this question to come up with another alternative solution.
Goal: To always bundle the files, to disable the JS and CSS minification in the event that <compilation debug="true" ... /> and to always apply a custom transformation to the CSS bundle.
My solution:
1) In web.config:
<compilation debug="true" ... />
2) In the Global.asax Application_Start() method:
protected void Application_Start() {
...
BundleTable.EnableOptimizations = true; // Force bundling to occur
// If the compilation node in web.config indicates debugging mode is enabled
// then clear all transforms. I.e. disable Js and CSS minification.
if (HttpContext.Current.IsDebuggingEnabled) {
BundleTable.Bundles.ToList().ForEach(b => b.Transforms.Clear());
}
// Add a custom CSS bundle transformer. In my case the transformer replaces a
// token in the CSS file with an AppConfig value representing the website URL
// in the current environment. E.g. www.mydevwebsite in Dev and
// www.myprodwebsite.com in Production.
BundleTable.Bundles.ToList()
.FindAll(x => x.GetType() == typeof(StyleBundle))
.ForEach(b => b.Transforms.Add(new MyStyleBundleTransformer()));
...
}
If you set the following property to false then it will disable both bundling and minification.
In Global.asax.cs file, add the line as mentioned below
protected void Application_Start()
{
System.Web.Optimization.BundleTable.EnableOptimizations = false;
}
Here's how to disable minification on a per-bundle basis:
bundles.Add(new StyleBundleRaw("~/Content/foobarcss").Include("/some/path/foobar.css"));
bundles.Add(new ScriptBundleRaw("~/Bundles/foobarjs").Include("/some/path/foobar.js"));
Sidenote: The paths used for your bundles must not coincide with any actual path in your published builds otherwise nothing will work. Also make sure to avoid using .js, .css and/or '.' and '_' anywhere in the name of the bundle. Keep the name as simple and as straightforward as possible, like in the example above.
The helper classes are shown below. Notice that in order to make these classes future-proof we surgically remove the js/css minifying instances instead of using .clear() and we also insert a mime-type-setter transformation without which production builds are bound to run into trouble especially when it comes to properly handing over css-bundles (firefox and chrome reject css bundles with mime-type set to "text/html" which is the default):
internal sealed class StyleBundleRaw : StyleBundle
{
private static readonly BundleMimeType CssContentMimeType = new BundleMimeType("text/css");
public StyleBundleRaw(string virtualPath) : this(virtualPath, cdnPath: null)
{
}
public StyleBundleRaw(string virtualPath, string cdnPath) : base(virtualPath, cdnPath)
{
Transforms.Add(CssContentMimeType); //0 vital
Transforms.Remove(Transforms.FirstOrDefault(x => x is CssMinify)); //0
}
//0 the guys at redmond in their infinite wisdom plugged the mimetype "text/css" right into cssminify upon unwiring the minifier we
// need to somehow reenable the cssbundle to specify its mimetype otherwise it will advertise itself as html and wont load
}
internal sealed class ScriptBundleRaw : ScriptBundle
{
private static readonly BundleMimeType JsContentMimeType = new BundleMimeType("text/javascript");
public ScriptBundleRaw(string virtualPath) : this(virtualPath, cdnPath: null)
{
}
public ScriptBundleRaw(string virtualPath, string cdnPath) : base(virtualPath, cdnPath)
{
Transforms.Add(JsContentMimeType); //0 vital
Transforms.Remove(Transforms.FirstOrDefault(x => x is JsMinify)); //0
}
//0 the guys at redmond in their infinite wisdom plugged the mimetype "text/javascript" right into jsminify upon unwiring the minifier we need
// to somehow reenable the jsbundle to specify its mimetype otherwise it will advertise itself as html causing it to be become unloadable by the browsers in published production builds
}
internal sealed class BundleMimeType : IBundleTransform
{
private readonly string _mimeType;
public BundleMimeType(string mimeType) { _mimeType = mimeType; }
public void Process(BundleContext context, BundleResponse response)
{
if (context == null)
throw new ArgumentNullException(nameof(context));
if (response == null)
throw new ArgumentNullException(nameof(response));
response.ContentType = _mimeType;
}
}
To make this whole thing work you need to install (via nuget):
WebGrease 1.6.0+
Microsoft.AspNet.Web.Optimization 1.1.3+
And your web.config should be enriched like so:
<runtime>
[...]
<dependentAssembly>
<assemblyIdentity name="System.Web.Optimization" publicKeyToken="31bf3856ad364e35" />
<bindingRedirect oldVersion="1.0.0.0-x.y.z.t" newVersion="x.y.z.t" />
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="WebGrease" publicKeyToken="31bf3856ad364e35" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-x.y.z.t" newVersion="x.y.z.t" />
</dependentAssembly>
[...]
</runtime>
<!-- setting mimetypes like we do right below is absolutely vital for published builds because for some reason the -->
<!-- iis servers in production environments somehow dont know how to handle otf eot and other font related files -->
<system.webServer>
[...]
<staticContent>
<!-- in case iis already has these mime types -->
<remove fileExtension=".otf" />
<remove fileExtension=".eot" />
<remove fileExtension=".ttf" />
<remove fileExtension=".woff" />
<remove fileExtension=".woff2" />
<mimeMap fileExtension=".otf" mimeType="font/otf" />
<mimeMap fileExtension=".eot" mimeType="application/vnd.ms-fontobject" />
<mimeMap fileExtension=".ttf" mimeType="application/octet-stream" />
<mimeMap fileExtension=".woff" mimeType="application/font-woff" />
<mimeMap fileExtension=".woff2" mimeType="application/font-woff2" />
</staticContent>
<!-- also vital otherwise published builds wont work https://stackoverflow.com/a/13597128/863651 -->
<modules runAllManagedModulesForAllRequests="true">
<remove name="BundleModule" />
<add name="BundleModule" type="System.Web.Optimization.BundleModule" />
</modules>
[...]
</system.webServer>
Note that you might have to take extra steps to make your css-bundles work in terms of fonts etc. But that's a different story.
Search for EnableOptimizations keyword in your project
So if you find
BundleTable.EnableOptimizations = true;
turn it false.
This does disable minification,
And it also disables bundling entirely
Just to supplement the answers already given, if you also want to NOT minify/obfuscate/concatenate SOME files while still allowing full bundling and minification for other files the best option is to go with a custom renderer which will read the contents of a particular bundle(s) and render the files in the page rather than render the bundle's virtual path. I personally required this because IE 9 was $*%#ing the bed when my CSS files were being bundled even with minification turned off.
Thanks very much to this article, which gave me the starting point for the code which I used to create a CSS Renderer which would render the files for the CSS but still allow the system to render my javascript files bundled/minified/obfuscated.
Created the static helper class:
using System;
using System.Text;
using System.Web;
using System.Web.Mvc;
using System.Web.Optimization;
namespace Helpers
{
public static class OptionalCssBundler
{
const string CssTemplate = "<link href=\"{0}\" rel=\"stylesheet\" type=\"text/css\" />";
public static MvcHtmlString ResolveBundleUrl(string bundleUrl, bool bundle)
{
return bundle ? BundledFiles(BundleTable.Bundles.ResolveBundleUrl(bundleUrl)) : UnbundledFiles(bundleUrl);
}
private static MvcHtmlString BundledFiles(string bundleVirtualPath)
{
return new MvcHtmlString(string.Format(CssTemplate, bundleVirtualPath));
}
private static MvcHtmlString UnbundledFiles(string bundleUrl)
{
var bundle = BundleTable.Bundles.GetBundleFor(bundleUrl);
StringBuilder sb = new StringBuilder();
var urlHelper = new UrlHelper(HttpContext.Current.Request.RequestContext);
foreach (BundleFile file in bundle.EnumerateFiles(new BundleContext(new HttpContextWrapper(HttpContext.Current), BundleTable.Bundles, bundleUrl)))
{
sb.AppendFormat(CssTemplate + Environment.NewLine, urlHelper.Content(file.VirtualFile.VirtualPath));
}
return new MvcHtmlString(sb.ToString());
}
public static MvcHtmlString Render(string bundleUrl, bool bundle)
{
return ResolveBundleUrl(bundleUrl, bundle);
}
}
}
Then in the razor layout file:
#OptionalCssBundler.Render("~/Content/css", false)
instead of the standard:
#Styles.Render("~/Content/css")
I am sure creating an optional renderer for javascript files would need little to update to this helper as well.
If you're using LESS/SASS CSS transformation there's an option useNativeMinification which can be set to false to disable minification (in web.config). For my purposes I just change it here when I need to, but you could use web.config transformations to always enable it on release build or perhaps find a way modify it in code.
<less useNativeMinification="false" ieCompat="true" strictMath="false"
strictUnits="false" dumpLineNumbers="None">
Tip: The whole point of this is to view your CSS, which you can do in the browser inspect tools or by just opening the file. When bundling is enabled that filename changes on every compile so I put the following at the top of my page so I can view my compiled CSS eaily in a new browser window every time it changes.
#if (Debugger.IsAttached)
{
View CSS
}
this will be a dynamic URL something like https://example.com/Content/css/bundlename?v=UGd0FjvFJz3ETxlNN9NVqNOeYMRrOkQAkYtB04KisCQ1
Update: I created a web.config transformation to set it to true for me during deployment / release build
<bundleTransformer xmlns="http://tempuri.org/BundleTransformer.Configuration.xsd">
<less xdt:Transform="Replace" useNativeMinification="true" ieCompat="true" strictMath="false" strictUnits="false" dumpLineNumbers="None">
<jsEngine name="MsieJsEngine" />
</less>
</bundleTransformer>
This may become useful to someone in the future as the new framework, when setup through VS, gets a default web.config, web.Debug.config and web.Release.config. In the web.release.config you will find this line:
<compilation xdt:Transform="RemoveAttributes(debug)" />
this was seeming to override any inline changes I made. I commented this line out and we were gravy (in terms of seeing non-minified code in a "release" build)

Custom Virtual Path Provider in IIS

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.

Resources