ASP.NET using embedded resources in Bundling
Asked Answered
T

2

15

I'm trying to implement a generic approach for providing the possibility for different assemblies in my web solution to use embedded JavaScript and CSS files from embedded resources. This blog post shows a technique using a VirtualPathProvider. This works fine, but the VirtualPathProvider needs to be included in each assembly containing embedded resources.

I tried to enhance the VirtualPathProvider from the blog post, so that an assembly can be passed into it and it loads the resource from its assembly:

public EmbeddedVirtualPathProvider(VirtualPathProvider previous, Assembly assembly)
{
    this.previous = previous;
    this.assembly = assembly;
}

On initialization it reads all embedded resources from the passed assembly:

protected override void Initialize()
{
    base.Initialize();

    this.assemblyResourceNames = this.assembly.GetManifestResourceNames();
    this.assemblyName = this.assembly.GetName().Name;
}

And the GetFilereads the content from the passed assembly:

public override VirtualFile GetFile(string virtualPath)
{
    if (IsEmbeddedPath(virtualPath))
    {
        if (virtualPath.StartsWith("~", System.StringComparison.OrdinalIgnoreCase))
        {
            virtualPath = virtualPath.Substring(1);
        }

        if (!virtualPath.StartsWith("/", System.StringComparison.OrdinalIgnoreCase))
        {
            virtualPath = string.Concat("/", virtualPath);
        }

        var resourceName = string.Concat(this.assembly.GetName().Name, virtualPath.Replace("/", "."));
        var stream = this.assembly.GetManifestResourceStream(resourceName);

        if (stream != null)
        {
            return new EmbeddedVirtualFile(virtualPath, stream);
        }
        else
        {
            return _previous.GetFile(virtualPath);
        }
    }
    else
        return _previous.GetFile(virtualPath);
}

Checking if resource is an embedded resource of this assembly is by checking the resource names read in the Initialize method:

private bool IsEmbeddedPath(string path)
{
    var resourceName = string.Concat(this.assemblyName, path.TrimStart('~').Replace("/", "."));
    return this.assemblyResourceNames.Contains(resourceName, StringComparer.OrdinalIgnoreCase);
}

I moved the EmbeddedVirtualPathProvider class to the main web project (ProjectA), so that it doesn't need to be included in each assembly containing embedded resources and registered it using the following code in Global.asax:

HostingEnvironment.RegisterVirtualPathProvider(
    new EmbeddedVirtualPathProvider(
        HostingEnvironment.VirtualPathProvider,
        typeof(ProjectB.SomeType).Assembly));

In the project containing the embedded resources (ProjectB) I still create the following bundle in a PostApplicationStartMethod:

 BundleTable.Bundles.Add(new ScriptBundle("~/Embedded/Js")
     .Include("~/Scripts/SomeFolder/MyScript.js")
 );

Scripts/MyScript.js is the embedded resource in ProjectB.

With this I receive the following exception:

Directory 'C:\webs\ProjectA\Scripts\SomeFolder\' does not exist. Failed to start monitoring file changes.

Update Full stack trace available in this Gist.

Update Also the VirtualPathProvider itself seems to work fine. If I load the file directly and not through the bundle and set the following entry in the web.config it loads the embedded javascript from ProjectB:

<system.webServer>
  <handlers>
    <add name="MyStaticFileHandler" path="*.js" verb="GET,HEAD" type="System.Web.StaticFileHandler"/>
  </handlers>
</system.webServer>
Teri answered 14/1, 2016 at 10:30 Comment(7)
Where is your Startup class?In ProjectA or in ProjectB?Chifley
ProjectA is a NuGet Package containing the VirtualPathProvider. ProjectB another NuGet Package providing some functionality with views (there are multiple of it). The NuGet Package ProjectB has a dependency on the NuGetPackage ProjectA. Applications install the ProjectB NuGetPackages. Therefore Startup is outside of ProjectA and ProjectB, but ProjectA and ProjectB can hook into PreApplicationStartMethod.Teri
It seems that IsEmbeddedPath method return false whereas it should return true. Could you tell us the value of path and resourceName before the error occured ?Straightaway
If I debug IsEmbeddedPath returns true and GetFile is also called and returns the stream of the embedded resource.Teri
@PascalBerger IsEmbeddedPath always return true. By looking at the call stack, we can see that GetCacheDependency is called and then the base GetCacheDependency is called which means that IsEmbeddedPath return false one time.Straightaway
If I set a breakpoint in FileExists with a condition of virtualPath.Contains("MyScript.js"), if it is called virtualPath is set to ~/Scripts/SomeFolder/MyScript.js. In IsEmbeddedPath the resourceName is ProjectA.SomeFolder.MyScript.js which exists in assemblyResourceNames and therefore true is returned. I've also another breakpoint with the same condition set in GetFile which is called afterwards. And the VirtualFile object for the resource is returned from there. Afterwards no further calls to either FileExists or GetFile happenTeri
@CyrilDurand The VirtualPathProvider itself seems to work fine (see updated question). If I directly load the JS file, don't use a bundle, and declare all *.js files as static files it finds the embedded JavaScript file.Teri
S
1

When ASP.net optimization create the bundle it call GetCacheDependency for the virtual directory of the script. Your GetCacheDependency implementation only check virtual file, for virtual directory it relies on the base VirtualPathProvider which check if directory exists and failed.

To solve this issue, you have to check if the path is a directory of one of your script and return null for the GetCacheDependency.

To safely determine if virtualPath is a bundle directory, you can use the BundleTable.Bundles collection or using a convention (ie: every bundle should starts with ~/Embedded).

public override CacheDependency GetCacheDependency(
    string virtualPath, 
    IEnumerable virtualPathDependencies, 
    DateTime utcStart)
{
    // if(virtualPath.StartsWith("~/Embedded"))
    if(BundleTables.Bundles.Any(b => b.Path == virtualPath))
    {
        return null; 
    }
    if (this.IsEmbeddedPath(virtualPath))
    {
        return null;
    }
    else
    {
        return this._previous
                   .GetCacheDependency(virtualPath, virtualPathDependencies, utcStart);
    }
}
Straightaway answered 22/1, 2016 at 15:11 Comment(3)
Thanks. I tried this but GetCacheDependency is not called for C:\webs\ProjectA\Scripts\SomeFolder` only for ~/Embedded/Js`.Teri
@PascalBerger When the exception occurs what is the value of virtualPath ?Straightaway
Ah, sorry. Was an error in my file/path detection. If I fix check for ~/Embedded/Js and return null it works. Now I need to only find a safe way to determine if virtualPath is a directory or file :)Teri
G
1

Regarding below error

Directory 'C:\webs\ProjectA\Scripts\SomeFolder\' does not exist. Failed to start monitoring file changes.

This happens specifically if all resource files of the SomeFolder are embedded and thus in published site - it does not have this folder created.

In case of bundle - it keeps timestamp when the bundle is created and it monitors the folder for any file change to trigger update in the bundle file.

Here - no files in the SomeFolder to monitor - as all are embedded. Didn't find to prevent the folder monitoring - but by handling this specific exception, it can be ignored.

Granddaddy answered 14/6, 2020 at 10:46 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.