Robert
Robert

Reputation: 105

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.

Upvotes: 4

Views: 8791

Answers (4)

riofly
riofly

Reputation: 1775

I developed a code that "rewrite" the HostingEnvironment.RegisterVirtualPathProvider method.
So, it bypass the limit wrote in System.Web.Hosting library.

You can use VirtualPathHelper.ForceRegisterVirtualPathProvider to set a VirtualPath. Or you can call VirtualPathHelper.HackSystemHostingEnvironmentRegistration() method in application initialization to hack the HostingEnvironment.RegisterVirtualPathProvider, so any call of your code or of any library of project will be use your Registration variant.

/// <summary>
    /// Helper per la gestione dei VirtualPath
    /// </summary>
    public static class VirtualPathHelper
    {
        /// <summary>
        /// Indica se il sito web è precompilato.
        /// </summary>
        public static bool IsPrecompiledApp
        {
            get { return System.Web.Compilation.BuildManager.IsPrecompiledApp; }
        }

        /// <summary>
        /// Forza la registrazione del <paramref name="virtualPathProvider"/>, in quanto funziona anche se
        /// il progetto Web è "precompilato".
        /// Per dettagli sul problema vedere qui: https://sunali.com/2008/01/09/virtualpathprovider-in-precompiled-web-sites/
        /// </summary>
        /// <param name="virtualPathProvider"></param>
        public static void ForceRegisterVirtualPathProvider(VirtualPathProvider virtualPathProvider)
        {
            if (!IsPrecompiledApp)
            {
                // Sito non Precompilato.
                // Non serve alcun Hack.
                System.Web.Hosting.HostingEnvironment.RegisterVirtualPathProvider(virtualPathProvider);
            }
            else
            {
                
                // we get the current instance of HostingEnvironment class. We can't create a new one
                // because it is not allowed to do so. An AppDomain can only have one HostingEnvironment
                // instance.
                var hostingEnvironmentInstance = (System.Web.Hosting.HostingEnvironment)typeof(System.Web.Hosting.HostingEnvironment)
                    .InvokeMember("_theHostingEnvironment", BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.GetField, null, null, null);
                if (hostingEnvironmentInstance == null)
                {
                    return;
                }

                // we get the MethodInfo for RegisterVirtualPathProviderInternal method which is internal
                // and also static.
                MethodInfo mi = typeof(System.Web.Hosting.HostingEnvironment).GetMethod("RegisterVirtualPathProviderInternal", BindingFlags.NonPublic | BindingFlags.Static);
                if (mi == null)
                {
                    return;
                }

                // finally we invoke RegisterVirtualPathProviderInternal method with one argument which
                // is the instance of our own VirtualPathProvider.
                mi.Invoke(hostingEnvironmentInstance, new object[] { virtualPathProvider });
            }
        }

        /// <summary>
        /// Modifica il puntatore al metodo <see cref="System.Web.Hosting.HostingEnvironment.RegisterVirtualPathProvider"/> per portare a compimento di tutte le chiamate,
        /// anche in condizione di `<see cref="IsPrecompiledApp"/> = True`.
        /// </summary>
        public static void HackSystemHostingEnvironmentRegistration()
        {
            if (!IsPrecompiledApp)
            {
                // Sito non Precompilato.
                // Non serve alcun Hack.
                return;
            }

            // Ottieni il tipo della classe originale
            var originalMethod = typeof(System.Web.Hosting.HostingEnvironment).GetMethod("RegisterVirtualPathProvider");

            // Ottieni il metodo alternativo (quello che vogliamo eseguire al posto dell'originale)
            var newMethod = typeof(VirtualPathHelper).GetMethod("ForceRegisterVirtualPathProvider");

            // Prepara i metodi per il JIT (necessario per manipolare i puntatori)
            System.Runtime.CompilerServices.RuntimeHelpers.PrepareMethod(originalMethod.MethodHandle);
            System.Runtime.CompilerServices.RuntimeHelpers.PrepareMethod(newMethod.MethodHandle);

            // Ottieni i puntatori ai metodi originali
            unsafe
            {
                if (IntPtr.Size == 8) // x64
                {
                    // Ottieni il puntatore alla funzione originale e alla nuova
                    var originalPointer = (long*)originalMethod.MethodHandle.Value.ToPointer() + 1;
                    var newPointer = (long*)newMethod.MethodHandle.Value.ToPointer() + 1;

                    // Sostituisci il puntatore della funzione originale con il puntatore alla nuova funzione
                    *originalPointer = *newPointer;
                }
                else // x86
                {
                    var originalPointer = (int*)originalMethod.MethodHandle.Value.ToPointer() + 1;
                    var newPointer = (int*)newMethod.MethodHandle.Value.ToPointer() + 1;

                    *originalPointer = *newPointer;
                }
            }
        }
    }

Upvotes: 0

Adam Carr
Adam Carr

Reputation: 2986

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()});

Upvotes: 6

Emil G
Emil G

Reputation: 1290

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>

Upvotes: 5

Tom Clarkson
Tom Clarkson

Reputation: 16174

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.

Upvotes: 1

Related Questions