Виртуальные пути

179

Между виртуальными и физическими путями существует стандартное отображение. Виртуальный путь наподобие /Content/RequestReporter.aspx соответствует файлу веб-формы /Content/RequestReporter.aspx. Главное преимущество такого отображения заключается в простоте - достаточно взглянуть на URL и немедленно понять, каким образом виртуальный путь будет применяться для генерации ответа.

Однако прямое отображение между виртуальными и физическими путями не всегда идеально. Например, модернизация приложения может привести к перемещению и переименованию файлов, из-за чего перестанут работать URL, помещенные пользователями в закладки. Может потребоваться согласование приложения Web Forms с более широкой схемой URL, которая не соответствует стандартному подходу. Подобная ситуация распространена в корпоративных приложениях, в которых согласованность URL расценивается как способ помочь пользователям в переходе непосредственно к нужному контенту или функциональности.

Короче говоря, существует множество причин для изменения URL, на которые реагирует приложение, и все они приводят к нарушению работы прямых ссылок между виртуальными и физическими путями. В последующих разделах мы опишем разнообразные подходы, доступные для управления виртуальными путями и позволяющие настраивать схему URL, которую приложение раскрывает внешнему миру.

Вы узнаете, что есть много способов достижения того же самого базового результата. Причина в том, что все эти средства постепенно добавлялись с течением времени. Самое последнее добавление - поддержка маршрутизации URL - является тем средством, с которого следует начинать. Однако мы также рассматриваем ряд удобных старых средств, т.к. часто требуется комбинировать одно из них с маршрутизацией URL, чтобы создавать специфическую функциональность в приложении.

Установка стандартных документов

По соглашению, принятому для приложений Web Forms, начальной веб-форме назначается имя Default.aspx. Это не является требованием, но его нужно придерживаться, поскольку сервер IIS сконфигурирован так, что он ищет стандартные файлы, если никакого файла в URL не указано.

Чтобы продемонстрировать это в действии, мы внесли в метод ProcessRequest(), определенный в файле SimpleModule.cs (который мы создали в предыдущей статье), небольшое изменение, которое обеспечивает вывод значения свойства FilePath и URL в окно Output среды Visual Studio. Изменение показано в примере ниже:

// ...
private void ProcessRequest(HttpApplication app)
{
    if (app.Request.FilePath == "/Test.aspx")
    {
        app.Server.Transfer("/Content/RequestReporter.aspx");
    }
    WriteMsg("URL запроса: {0} {1}", app.Request.RawUrl, app.Request.FilePath);
}
// ...

Для тестирования встроенного поведения запустите приложение и запросите корневой URL (у нас он выглядит как http://localhost:32404/, но у вас может быть другой номер порта). Браузер отобразит содержимое файла Default.aspx, а в окне Output будут отображаться следующие сообщения:

URL запроса: / /
URL запроса: / /
URL запроса: / /default.aspx

Последнее из этих сообщений отражает попытку сервера IIS найти файл, с помощью которого должен быть обслужен запрос. (Первые два сообщения объясняются в следующем разделе.) Сервер IIS имеет возможность отыскать файл Default.aspx для обслуживания запроса корневого URL, т.к. мы соблюдали соглашение об именовании. В противном случае IIS Express потерпел бы неудачу и возвратил браузеру ошибку 404, указывающую на то, что файл найти не удалось.

Сервер IIS ищет следующие стандартные документы: Default.html, Default.asp, index.htm, index.html, iisstart.htm и, наконец, default.aspx. (Мы не знаем, почему имя файла default.aspx представлено символами нижнего регистра, но это не играет роли, потому что имена веб-форм нечувствительны к регистру.)

Переопределить эти стандартные документы можно в файле Web.config. Это стоит делать, если нужно, чтобы по умолчанию использовалась другая стандартная веб-форма, или требуется сократить количество местоположений, в которые сервер IIS просматривает, прежде чем передает веб-форму на обработку среде ASP.NET.

В примере ниже показаны изменения, внесенные в файл Web.config:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  ...
  <system.webServer>
    ...
    <defaultDocument enabled="true">
      <files>
        <clear />
        <add value="Content/RequestReporter.aspx"/>
      </files>
    </defaultDocument>
  </system.webServer>
</configuration>

Элемент defaultDocument добавлен в раздел <system.webserver> конфигурационного файла. В нем определен атрибут enabled, который по умолчанию установлен в true (значение false предотвратит попытки поиска сервером IIS стандартного документа).

Внутри defaultDocument содержится элемент <file>, представляющий собой коллекцию стандартных документов, которые будет искать IIS. С помощью элемента <clear> мы удалили все стандартные документы, а посредством элемента <add> задали специальную политику. В элементе defaultDocument/files/add определен единственный атрибут по имени value, применяемый для указания файла, который IIS должен искать. Мы воспользовались элементом add для установки веб-формы RequestReporter.aspx из папки Content в качестве единственного стандартного документа.

Обратите внимание, что при указании стандартного документа в атрибуте value ведущий символ / отсутствует. Добавление ведущего символа / приводит к отображению сообщения об ошибке.

Чтобы увидеть результат, запустите приложение и запросите URL вида http://localhost:<порт>/, где <порт> это номер порта, прослушиваемого сервером IIS Express на предмет поступления запросов для этого приложения. Новая политика в отношении стандартного документа будет применена, и отобразится вывод, сгенерированный веб-формой RequestReporter.aspx.

Обработка запросов для URL без расширений

При отправке запроса к корневому URL в предыдущем разделе в окне Output среды Visual Studio отображались три сообщения:

URL запроса: / /
URL запроса: / /
URL запроса: / /default.aspx

Мы объяснили, что последнее сообщение говорит о применении политики IIS, касающейся стандартного документа. Первоначально это был запрос к Default.aspx, но мы изменили политику IIS в отношении стандартного документа. Перед тем, как IIS применяет такую политику, он предоставляет ASP.NET шанс обработать запрос - это причина, по которой отображено первое сообщение.

Среда ASP.NET имеет обработчик для этого запроса, однако по умолчанию он не делает ничего полезного. Таким обработчиком является внутренний класс TransferRequestHandler, отвечающий за обработку URL без расширений, которые позволяют ASP.NET обрабатывать запросы к виртуальным путям, не содержащим файловые расширения, такие как .aspx.

Класс TransferRequestHandler не делает особенно много с запросами для URL без расширений. Он просто указывает серверу IIS на необходимость создания и обработки второго запроса, не используя TransferRequestHandler в качестве обработчика - именно поэтому отображается второе сообщение.

Чтобы делать нечто полезное с запросами для URL без расширений, понадобится создать обработчик и заменить им TransferRequestHandler. Мы добавили в проект новый файл класса по имени ExtensionlessHandler.cs, содержимое которого приведено в примере ниже:

using System.IO;
using System.Web;

namespace PathsAndURLs
{
    public class ExtensionlessHandler : IHttpHandler
    {

        public void ProcessRequest(HttpContext context)
        {
            context.Response.Write("<p>HTTP-обработчик Expressionless</p>");
            string vpath = context.Request.Path;
            if (vpath == "/")
            {
                context.Server.Transfer("/Default.aspx");
            }
            else if (File.Exists(context.Request.MapPath(vpath + ".aspx")))
            {
                context.Server.Transfer(vpath + ".aspx");
            }
            else
            {
                context.Response.StatusCode = 404;
                context.ApplicationInstance.CompleteRequest();
            }
        }

        public bool IsReusable
        {
            get { return false; }
        }
    }
}

Этот обработчик будет получать запросы для URL без расширений и с помощью метода HttpServerUtility.Transfer() передавать запросы веб-форме. Определение, какой веб-форме должен быть передан запрос, реализуется элементарно. Если запрошенным URL является /, мы направляем его Default.aspx, а для всех других запросов просто добавляем расширение .aspx к запрошенному URL и проверяем, существует ли в приложении файл с таким именем. Если это так, мы передаем ему запрос, а в противном случае возвращаем ответ с ошибкой 404.

Чтобы обработчик смог принимать запросы, его необходимо зарегистрировать. В примере ниже показаны изменения, внесенные в файл Web.config:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <system.web>
    <compilation debug="true" targetFramework="4.5.1" />
    <httpRuntime targetFramework="4.5.1" />
  </system.web>
  <system.webServer>
    <handlers>
      <add name="ExtensionLess" path="*." verb="*"
           type="PathsAndURLs.ExtensionlessHandler"/>
    </handlers>
    ...
  </system.webServer>
</configuration>

Для обработки URL без расширений мы устанавливаем атрибут path в "*." (символы звездочки и точки). Обработка URL без расширений выполняется перед применением политики IIS, касающейся стандартного документа, поэтому предыдущая конфигурация была переопределена и корневой URL (/) отображен на Default.aspx. В качестве дополнения мы можем запрашивать любую веб-форму, не указывая файловое расширение. Таким образом, например, запрос для виртуального пути /Content/RequestReporter сгенерирует ответ на основе веб-формы /Content/RequestReporter.aspx.

Переписывание путей

В предыдущем примере использовался метод HttpServerUtility.Transfer(), который нормально работает с веб-формами, но не очень хорошо с другими типами файлов, такими как обобщенные обработчики (файлы ashx). Мы могли бы применить прием с оболочкой для Page, но это грубый трюк, и мы не являемся его сторонниками.

Имея все это в виду, рассматриваемый далее подход может быть реализован более широко, однако это должно делаться внутри модуля. Прием называется переписыванием путей и представляет собой просто процесс изменения пути, связанного с запросом. Чтобы показать пример использования такого подхода, мы создали файл класса по имени PathModule.cs с содержимым, приведенным в примере ниже:

using System;
using System.IO;
using System.Web;

namespace PathsAndURLs
{
    public class PathModule : IHttpModule
    {
        private static readonly string[] extensions = { ".aspx", ".ashx" };
        public void Init(HttpApplication app)
        {
            app.BeginRequest += (src, args) => HandleRequest(app);
        }

        private void HandleRequest(HttpApplication app)
        {
            if (app.Request.CurrentExecutionFilePathExtension == String.Empty)
            {
                string target = null;
                string vpath = app.Request.CurrentExecutionFilePath;

                if (vpath == "/")
                {
                    target = "/Default.aspx";
                }
                else
                {
                    foreach (string ext in extensions)
                    {
                        if (File.Exists(app.Request.MapPath(vpath + ext)))
                        {
                            target = vpath + ext;
                            break;
                        }
                    }
                }

                if (target != null)
                {
                    app.Context.RewritePath(target);
                }
            }
        }

        public void Dispose()
        {
            // Ничего не освобождается
        }
    }
}

Так как это модуль, его необходимо зарегистрировать в файле Web.config:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <system.web>
    <compilation debug="true" targetFramework="4.5.1" />
    <httpRuntime targetFramework="4.5.1" />
  </system.web>
  <system.webServer>
    ...
    <modules>
	  <add name="Rewriter" type="PathsAndURLs.PathModule"/>
      <add name="Simple" type="PathsAndURLs.SimpleModule"/>
    </modules>
    ...
  </system.webServer>
</configuration>

Этот модуль обрабатывает событие BeginRequest и просматривает запросы, которые не имеют файлового расширения. В рассматриваемом примере изменяется способ обработки корневого URL, т.е. для обработки запросов применяется веб-форма Default.aspx, которую можно увидеть, запустив приложение и запросив /.

Главное улучшение по сравнению с предыдущим примером связано с проверкой существования файлов, имеющих расширения aspx или ashx, если запрошенным URL не является /. Это позволяет приложению поддерживать дружественные URL - именно так называются запросы веб-форм и обработчиков без указания файловых расширений. (Происхождение данного названия нам не известно, однако мы его придерживаемся.)

Прием является вариацией обработки URL без расширений и по-прежнему требует сопоставления виртуальных путей с файлами в приложении, но без указания расширений .aspx или .ashx, которые многие разработчики считают непривлекательными.

Ключевым в модуле является метод RewritePath(), который определен в классе HttpContext. Этот метод позволяет изменить путь при условии, что это делается перед событием жизненного цикла MapRequestHandler.

Метод RewritePath() не имеет ограничений, с которыми мы сталкиваемся при использовании методов класса HttpServerUtility, т.е. появляется возможность поддерживать запросы к обобщенным обработчикам, а также файлам веб-форм.

В классе HttpContext определено несколько перегруженных версий метода RewritePath(), которые описаны в таблице ниже:

Перегруженные версии метода HttpContext.RewritePath()
Версия Описание
RewritePath(path)

Изменяет путь для текущего запроса

RewritePath (path, rebase)

Изменяет путь для текущего запроса, дополнительно выполняя изменения базы клиента

RewritePath (path, info, query)

Изменяет путь для текущего запроса, включая указанную информацию пути и строку запроса

RewritePath (path, info, query, rebase)

Изменяет путь для текущего запроса, включая указанную информацию пути и строку запроса и дополнительно выполняя изменения базы клиента

Две перегруженных версии метода RewritePath() принимают аргумент типа bool по имени rebase, который отвечает за изменение путей, используемых элементами управления для создания URL - процесс, известный как изменение базы клиента.

В Microsoft предлагается загружаемый пакет под названием URL Rewriting Engine (Механизм переписывания URL), который позволяет выражать правила переписывания в файле Web.config, а не в коде.

Реальный пример переписывания путей

Ранее метод HttpContext.RewritePath() использовался для добавления файлового расширения, т.е. мы могли бы поддерживать URL без расширений и дружественные URL. С помощью средства переписывания путей можно реализовать очень сложные действия, из-за чего мы применяем его в проектах, требующих поддержки необычных схем URL и таких, которые варьируются на основе характеристик запросов, отличных от путей.

В целях иллюстрации мы рассмотрим пример, основанный на одном из реальных проектов, над которым мы работали недавно. Нам необходимо было повторно построить приложение с применением ASP.NET, но старые URL были жестко закодированы в других приложениях и должны были остаться работоспособными.

Одна из проблем, с которой мы столкнулись, заключалась в том, что запросы к URL вида /accounts нужно было направлять двум веб-формам в зависимости от значения данных формы, имеющего имя function. Когда значение function не превышало 100, запрос должен отправляться веб-форме Default.aspx, а для всех других значений - веб-форме /Content/RequestReporter.aspx (разумеется, это не веб-формы из реального проекта; мы просто хотим воспользоваться уже готовыми файлами в примере приложения).

Чтобы продемонстрировать проблему, мы создали новую веб-форму по имени Split.aspx, контент которой представлен в примере ниже. Эта веб-форма будет эмулировать унаследованных клиентов с жестко закодированными URL:

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Split.aspx.cs" 
    Inherits="PathsAndURLs.Split" %>

<!DOCTYPE html>
<html>
<head runat="server">
    <title></title>
</head>
<body>
    <form action="/accounts" method="post">
        <div>
            Функция: <input name="function" value="100" />
        </div>
        <button type="submit">Отправить</button>
    </form>
</body>
</html>

Веб-форма содержит простую HTML-форму, которая позволяет ввести значение function и отправить его серверу по щелчку на кнопке "Отправить". Элемент <form> сконфигурирован так, что данные отправляются по пути /accounts, который не соответствует ни одному из файлов внутри проекта.

В примере ниже приведен модифицированный файл класса SimpleModule.cs, созданного ранее в проекте, в котором можно видеть, как обрабатывается такой запрос:

// ...
private void ProcessRequest(HttpApplication app)
{
    if (app.Request.Path == "/accounts")
    {
        int functionValue;
        if (int.TryParse(app.Request.Form["function"], out functionValue))
        {
            if (functionValue < 100)
            {
                app.Context.RewritePath("/Default.aspx");
            }
            else
            {
                app.Context.RewritePath("/Content/RequestReporter.aspx");
            }
        }
    }
    WriteMsg("URL запроса: {0} {1}", app.Request.RawUrl, app.Request.FilePath);
}
// ...

Вместо того чтобы просто дополнить путь файловым расширением, мы применяем метод RewritePath() для направления запроса на разные веб-формы на основе значения данных формы, поступившего вместе с запросом. Запустите приложение и запросите URL вида /Split. После отправки данных запрос попадает на URL /accounts и затем, в зависимости от значения данных формы, перенаправляется соответствующей веб-форме.

Пройди тесты
Лучший чат для C# программистов