Работа с путями

52

В этой и последующих статьях мы объясним, как ASP.NET Framework использует пути для сопоставления URL, на который указывает запрос, с файлом, применяемым для генерации ответа. Различают два типа путей - виртуальные и физические - и мы расскажем о роли каждого из них, а также о том, как они связаны друг с другом. Кроме того, мы продемонстрируем ряд базовых приемов управления путями, которые позволяют получить контроль над URL, поддерживаемыми приложением.

В качестве примера мы создаем новый проект под названием PathsAndURLs, используя шаблон ASP.NET Empty Web Application (Пустое веб-приложение ASP.NET). В проект мы добавили веб-форму по имени Default.aspx с разметкой, приведенной в примере ниже:

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

<!DOCTYPE html>
<html>
<head runat="server">
    <title></title>
</head>
<body>
    <p>Этой файл Default.aspx проекта ASP.NET Web Forms</p>
</body>
</html>

Эта базовая веб-форма отображает простое сообщение, содержащее ее имя. Это упростит освоение примеров по мере исследования отношений между URL и файлами, на которые они нацелены. Файл отделенного кода остается без изменений.

Щелкните правой кнопкой мыши на элементе Default.aspx в окне Solution Explorer и выберите в контекстном меню пункт Set As Start Page (Установить в качестве стартовой страницы). Если вы не сделаете этого, то получите результаты, отличающиеся от приведенных далее.

Мы добавили в проект файл класса C# по имени SimpleModule.cs и поместили в него реализацию интерфейса IHttpModule, показанную в примере ниже. Этот модуль обрабатывает событие BeginRequest, вызывая метод ProcessRequest(), который принимает объект HttpApplication:

using System.Diagnostics;
using System.Web;

namespace PathsAndURLs
{
    public class SimpleModule : IHttpModule
    {
        public void Init(HttpApplication app)
        {
            app.BeginRequest += (src, args) => ProcessRequest(app);
        }

        private void ProcessRequest(HttpApplication app)
        {
            WriteMsg("URL запроса: {0}", app.Request.RawUrl);
        }

        private void WriteMsg(string formatString, params object[] vals)
        {
            Debug.WriteLine(formatString, vals);
        }

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

Начальная реализация модуля реагирует на событие BeginRequest выводом сообщения в окно Output среды Visual Studio. Это сообщение отражает URL для текущего запроса, который извлекается из свойства HttpRequest.RawUrl. Перед использованием модуль должен быть зарегистрирован, в примере ниже представлено добавление в файл 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="Simple" type="PathsAndURLs.SimpleModule"/>
    </modules>
  </system.webServer>
</configuration>

Чтобы протестировать приложение, просто запустите его. Браузер запросит URL вида /Default.aspx, который сгенерирует ответ с помощью веб-формы Default.aspx и выведет в окно Output среды Visual Studio следующее сообщение:

Этой файл Default.aspx проекта ASP.NET Web Forms

Если вы видите три запроса к корневому URL (/), значит, вы забыли установить Default.aspx в качестве стандартной стартовой страницы. Множество наблюдаемых запросов отражают попытку сервера IIS найти стандартный документ для обслуживания вместе с малоизвестным средством ASP.NET, которое называется обработкой URL без расширений: оба эти аспекта будут объясняться позже.

Понятие путей

Путь (path) - это строка, применяемая для уникального указания ресурса. В ASP.NET приходится иметь дело с двумя видами путей - физические пути и виртуальные пути. Физический путь уникально идентифицирует файл в рамках файловой системы. Например, на нашей машине разработки физический путь к созданной ранее веб-форме выглядит следующим образом:

С:\Projects\PathsAndURLs\Default.aspx

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

http://localhost:32404/Default.aspx

то виртуальный путь будет таким:

/Default.aspx

Отображение между физическими и виртуальными путями является важным аспектом приложения Web Forms из-за особенностей использования отдельных файлов, таких как aspx, ashx и ascx, для генерации ответов. Когда мы создаем простую веб-форму или элемент управления, то обычно не заботимся о запрашиваемом файле, т.к. отображение виртуального пути на физический обеспечивают встроенные обработчики веб-форм.

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

Использование символа тильды (~)

Вы часто будете видеть, что виртуальные пути ASP.NET выражаются с применением символа тильды (~). Такие пути создают URL, которые являются относительными корневой папки приложения и называются URL, относящимися к приложению.

Можно создавать приложения ASP.NET, доступные через один и тот же корневой URL, так что URL вида http://site.com, http://site.com/hr и http://site.com/sales будут представлять разные веб-приложения. Веб-форма внутри приложения hr может использовать URL наподобие ~/Default.aspx для ссылки на виртуальный путь /hr/Default.aspx, и не запрашивать файл /Default.aspx, являющийся частью совершенно другого приложения. Приложения развертываются таким образом, что они могут разделять общее имя хоста и порт, а символ "~" означает, что приложение не должно иметь жестко закодированную информацию о том, как оно разворачивается.

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

Мы рекомендуем развертывать приложения изолированно, чтобы не нужно было постоянно помнить о символе "~". В случае платформы на основе облака или хостинга это делается легко, тогда как ситуация с хостингом на собственных серверах требует использования нескольких портов HTTP или множества серверов.

Получение информации о путях

В классе HttpRequest определен ряд удобных методов и свойств, которые можно применять для получения информации о путях, с которыми связаны запросы; все эти методы и свойства описаны в таблице ниже:

Методы и свойства класса HttpRequest, имеющие отношение к путям
Метод иди свойство Описание
ApplicationPath

Получает корневой виртуальный путь приложения, которым будет /, если только в одну и ту же структуру каталогов не было развернуто несколько приложений

AppRelativeCurrentExecutionFilePath

Возвращает виртуальный путь с применением нотации тильды (~)

CurrentExecutionFilePath

Получает виртуальный путь текущего запроса. Это значение обновляется, когда используются методы Transfer() или Execute(), определенные в классе HttpServerUtility

CurrentExecutionFilePathExtension

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

FilePath

Получает виртуальный путь текущего запроса, не включая информацию пути. Это значение не обновляется при вызове методов Transfer() или Execute()

MapPath(virtualPath)

Возвращает физический путь для указанного виртуального пути

Path

Получает виртуальный путь текущего запроса, включая информацию пути. Это значение не обновляется при вызове методов Transfer() или Execute()

PathInfo

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

PhysicalApplicationPath

Получает корневой физический путь для местоположения приложения; в нашей системе для рассматриваемого примера это С:\Projects\PathsAndURLs

PhysicalPath

Получает физический путь к файлу, на который указывает текущий запрос. Это свойство возвращает путь к файлу из исходного запроса и не обновляется в случае вызова методов Transfer() или Execute()

Свойства, связанные с путями, которые определены в классе HttpRequest, могут быть сгруппированы с использованием двух характеристик - всегда ли они возвращают исходный путь, связанный с запросом, и как обрабатывается дополнительная информация пути.

Получение фиксированной и динамической информации пути

Большинство значений, которые возвращают свойства, связанные с путями, относятся к запросу в том виде, в каком он был впервые получен и не обновлялся в результате вызова методов Execute() или Transfer(), определенных в классе HttpServerUtility.

Исключением является свойство CurrentExecutionFilePath, которое обновляется при переопределении обычного запроса. Такая комбинация статической и динамической информации пути удобна, когда определяются обработчики, которым необходимы какие-то сведения о том, что изначально запрашивалось. В качестве простого примера мы добавили в папку Content веб-форму по имени ReguestReporter.aspx:

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

<!DOCTYPE html>
<html>
<head runat="server">
    <title></title>
</head>
<body>
    <table>
        <tr>
            <td>Оригинальный путь</td>
            <td><%= Request.FilePath %></td>
        </tr>
        <tr>
            <td>Оригинальный физический путь к файлу</td>
            <td><%= Request.PhysicalPath %></td>
        </tr>
        <tr>
            <td>Текущий виртуальный путь</td>
            <td><%= Request.CurrentExecutionFilePath %></td>
        </tr>
        <tr>
            <td>Текущий виртуальный путь к файлу</td>
            <td><%= Server.MapPath(Request.CurrentExecutionFilePath) %></td>
        </tr>
    </table>
</body>
</html>

Свойства, описанные в таблице выше, применяются для отображения исходного виртуального и физического пути запроса, а также текущего виртуального пути. Свойства для получения текущего физического пути не предусмотрено, поэтому мы вызываем метод MapPath(), который преобразует виртуальный путь в физический. Имя для свойства CurrentExecutionFilePath выбрано неудачно, поскольку оно возвращает виртуальный путь, а не физический, как можно было предположить.

Для демонстрации отличий между исходными и текущими путями мы модифицировали метод ProcessRequest() в файле класса SimpleModule.cs, вызвав в нем метод HttpServerUtility.Transfer(), когда виртуальным путем для запроса является /Test.aspx, как показано в примере ниже:

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

Чтобы увидеть отличия между путями, запустите приложение и запросите URL вида /Test.aspx. Веб-формы Test.aspx не существует, но запрос будет перехвачен и перемещен модулем, давая в результате вывод, представленный на рисунке ниже. (Конкретные физические пути зависят от того, где был создан проект Visual Studio.)

Результат применения метода HttpServerUtility.Transfer() для виртуального и физического пути

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

Второй момент связан с тем, что важно обращать максимальное внимание на то, где получается информация пути. Следует иметь четкое представление о том, что требуется - пути, которые были запрошены, или пути, которые ASP.NET использует для генерации ответа.

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

Обработка дополнительной информации пути

Когда среда ASP.NET производит разбор URL, она по очереди просматривает каждый его сегмент (текст между парой символов /), пока не найдет такой, который содержит точку. Она предполагает, что это запрашиваемый файл. Среда ASP.NET обрабатывает URL слева направо.

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

Вдобавок URL могут содержать сегменты, которые находятся после имени файла, например:

http://localhost:32404/Content/RequestReporter.aspx/One/Two/Three

Комбинация таких сегментов называется дополнительной информацией пути, или просто информацией пути. В приведенном выше URL информация пути выглядит как "/One/Two/Three". Эту информацию пути можно использовать для предоставления значений данных веб-формам и другим обработчикам или же игнорировать ее. Выбор влияет на то, какое свойство класса HttpRequest будет применяться для получения сведений о пути.

В таблице ниже перечислены свойства класса HttpRequest и значения, которые они возвращают для показанного выше URL:

Свойства класса HttpRequest и результаты, которые они возвращают для примера URL
Свойство Результат
ApplicationPath

/

AppRelativeCurrentExecutionFilePath

~/Content/RequestReporter.aspx

CurrentExecutionFilePath

/Content/RequestReporter.aspx (но будет изменяться в результате вызова методов Transfer() или Execute())

CurrentExecutionFilePathExtension

.aspx (но может измениться в результате вызова методов Transfer() или Execute())

FilePath

/Content/RequestReporter.aspx

Path

/Content/RequestReporter.aspx/One/Two/Three

PathInfo

/One/Two/Three

PhysicalApplicationPath

С:\Projects\PathsAndURLs\

PhysicalPath

С:\Projects\PathsAndURLs\Content\RequestReporter.aspx

Обратите внимание, что информация пути генерируется только для URL, которые обрабатываются ASP.NET, т.е. для URL, запрашивающих файлы типов, которые обслуживает ASP.NET, к примеру, aspx. В случае запроса URL вроде http://localhost:32404/Content/Colors.html/One/Two/Three виртуальным путем является /Content/Colors.html/One/Two/Three, a информация пути отсутствует.

Как показано в таблице, для получения требуемой информации пути важно выбирать правильные свойства, учитывая использование класса HttpServerUtility и наличие в URL дополнительной информации пути.

Манипулирование путями

В классе System.Web.VirtualPathUtility определен набор статических методов, с помощью которых можно манипулировать с путями. Использование этих методов предпочтительнее самостоятельной обработки строк путей, поскольку структура URL может оказаться довольно сложной.

Методы, определенные в классе VirtualPathUtility
Имя Описание
AppendTrailingSlash(path)

Возвращает указанный путь, добавляя завершающий символ /, если он отсутствует

Combine(base, path)

Комбинирует базовый и относительный путь, обеспечивая корректное количество символов /

GetDirectory(virtualPath)

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

GetExtension(virtualPath)

Возвращает расширение файла из указанного пути, включая ведущую точку

GetFileName(path)

Возвращает имя файла из указанного пути

IsAbsolute(virtualPath)

Возвращает true, если указанный путь начинается с /

IsAppRelative(virtualPath)

Возвращает true, если указанный путь начинается с ~

MakeRelative(from, to)

Возвращает первый путь, выраженный относительно второго пути

RemoveTrailingSlash(virtualPath)

Возвращает указанный путь, удаляя завершающий символ /, если он существовал

ToAbsolute(virtualPath)

Преобразует указанный относительный путь в абсолютный

ToAppRelative(virtualPath)

Преобразует указанный абсолютный путь в относительный

Некоторые из перечисленных методов позволяют упростить работу с путями, относящимися к приложению (которые начинаются с символа ~). Мы предпочитаем не пользоваться ими, однако считаем методы, предназначенные для извлечения определенных элементов и комбинирования путей, довольно удобными. Эти методы не отличаются особой сложностью, и вы легко можете воссоздать их в собственном коде. Тем не менее, следует отметить, что выполняемая ими работа является рутинной и требует многочисленных действий, связанных с разбором.

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