ASP.NET с использованием встроенных ресурсов в Bundling

Я пытаюсь реализовать общий подход для предоставления возможности для разных сборок в моем веб-решении использовать встроенные JavaScript файлы и CSS файлы из встроенных ресурсов. Этот пост в блоге показывает технику с использованием VirtualPathProvider. Это прекрасно работает, но VirtualPathProvider должен быть включен в каждую сборку, содержащую встроенные ресурсы.

Я попытался улучшить VirtualPathProvider из сообщения в блоге, так что сборка может быть передана в него, и он загружает ресурс из своей сборки:

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

При инициализации он считывает все внедренные ресурсы из прошедшей сборки:

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

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

И GetFile считывает содержимое из прошедшей сборки:

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

Проверка, является ли ресурс встроенным ресурсом этой сборки, путем проверки имен ресурсов, прочитанных в методе Initialize:

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

Я переместил класс EmbeddedVirtualPathProvider в основной веб-проект (ProjectA), так что его не нужно включать в каждую сборку, содержащую встроенные ресурсы, и зарегистрировать ее, используя следующий код в Global.asax:

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

В проекте, содержащем встроенные ресурсы (ProjectB), я по-прежнему создаю следующий пакет в PostApplicationStartMethod:

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

Scripts/MyScript.js - встроенный ресурс в ProjectB.

При этом я получаю следующее исключение:

Каталог 'C:\webs\ProjectA\Scripts\SomeFolder \' не существует. Не удалось запустить мониторинг изменений файла.

Обновление Полная трассировка стека доступна в this Gist.

Обновление Также сам VirtualPathProvider работает нормально. Если я загружаю файл напрямую, а не через пучок и устанавливаю следующий элемент в web.config, он загружает встроенный javascript из ProjectB:

<system.webServer>
  <handlers>
    <add name="MyStaticFileHandler" path="*.js" verb="GET,HEAD" type="System.Web.StaticFileHandler"/>
  </handlers>
</system.webServer>

Ответы

Ответ 1

Когда оптимизация ASP.net создает пакет, вызывайте GetCacheDependency для виртуального каталога script. Ваша реализация GetCacheDependency проверяет только виртуальный файл, для виртуального каталога он опирается на базу VirtualPathProvider, которая проверяет наличие и отсутствие каталога.

Чтобы решить эту проблему, вам нужно проверить, является ли путь каталогом одного из ваших script и возвращает null для GetCacheDependency.

Чтобы безопасно определить, является ли virtualPath каталогом пакетов, вы можете использовать коллекцию BundleTable.Bundles или использовать соглашение (то есть: каждый пучок должен начинаться с ~/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);
    }
}