Подтвердить что ты не робот

Можно ли использовать ASP.NET-маршрутизацию для создания "чистых" URL-адресов для обработчиков .ashx(IHttpHander)?

У меня есть некоторые службы REST, использующие простые старые IHttpHandler s. Я хотел бы создать более чистые URL-адреса, чтобы у меня не было .ashx в пути. Есть ли способ использовать маршрутизацию ASP.NET для создания маршрутов, которые сопоставляются с обработчиками ashx? Ранее я видел эти типы маршрутов:

// Route to an aspx page
RouteTable.Routes.MapPageRoute("route-name",
    "some/path/{arg}",
    "~/Pages/SomePage.aspx");

// Route for a WCF service
RouteTable.Routes.Add(new ServiceRoute("Services/SomeService",
    new WebServiceHostFactory(),
    typeof(SomeService)));

Попытка использовать RouteTable.Routes.MapPageRoute() создает ошибку (что обработчик не получается из Page). System.Web.Routing.RouteBase только, кажется, имеет 2 производных класса: ServiceRoute для служб и DynamicDataRoute для MVC. Я не уверен, что делает MapPageRoute() (Reflector не показывает тело метода, он просто показывает "Производительность, критичная для встроенного метода такого типа в границах изображения NGen" ).

Я вижу, что RouteBase не запечатан и имеет относительно простой интерфейс:

public abstract RouteData GetRouteData(HttpContextBase httpContext);

public abstract VirtualPathData GetVirtualPath(RequestContext requestContext,
    RouteValueDictionary values);

Так что, возможно, я могу создать свой собственный HttpHandlerRoute. Я дам этот снимок, но если кто-нибудь знает о существующем или встроенном способе сопоставления маршрутов с IHttpHandlers, это было бы здорово.

4b9b3361

Ответ 1

Хорошо, я понял это, так как я изначально задал этот вопрос, и у меня наконец есть решение, которое делает именно то, что я хочу. Однако, должно быть, немного объяснение переднего плана. IHttpHandler - очень простой интерфейс:

bool IsReusable { get; }
void ProcessRequest(HttpContext context)

Не существует встроенного свойства для доступа к данным маршрута, и данные маршрута также не могут быть найдены в контексте или запросе. Объект System.Web.UI.Page имеет свойство RouteData, ServiceRoute выполняет всю работу по интерпретации ваших UriTemplates и передаче значений правильному методу внутри, а ASP.NET MVC предоставляет свой собственный способ доступа к данным маршрута. Даже если у вас был RouteBase, который (а) определил, соответствует ли входящий URL-адрес вашему маршруту и ​​(б) проанализировал URL-адрес для извлечения всех отдельных значений, которые будут использоваться из вашего IHttpHandler, нет простого способа передайте данные маршрута вашему IHttpHandler. Если вы хотите, чтобы ваш IHttpHandler "чист", так сказать, он берет на себя ответственность за обращение с URL-адресом и извлечение из него любых значений. Реализация RouteBase в этом случае используется только для определения того, должен ли использоваться ваш IHttpHandler вообще.

Однако остается одна проблема. Как только RouteBase определяет, что входящий URL-адрес соответствует вашему маршруту, он переходит к IRouteHandler, который создает экземпляры IHttpHandler, которые вы хотите обработать вашим запросом. Но, как только вы находитесь в своем IHttpHandler, значение context.Request.CurrentExecutionFilePath вводит в заблуждение. Это URL-адрес, который пришел от клиента, за вычетом строки запроса. Так что это не путь к вашему .ashx файлу. И любые части вашего маршрута, которые являются постоянными (например, имя метода), будут частью этого пути пути к файлу. Это может быть проблемой, если вы используете UriTemplates в своем IHttpHandler, чтобы определить, какой конкретный метод в вашем IHttpHandler должен передать запрос.

Пример. Если у вас был обработчик .ashx в /myApp/services/myHelloWorldHandler.ashx И у вас был этот маршрут, который сопоставлен с обработчиком: "services/hello/{name}" И вы перешли к этому URL-адресу, пытаясь вызвать метод SayHello(string name) вашего обработчика: http://localhost/myApp/services/hello/SayHello/Sam

Тогда ваш CurrentExecutionFilePath будет:/myApp/services/hello/Sam. Он включает в себя части URL-адреса маршрута, что является проблемой. Вы хотите, чтобы путь к файлу выполнения соответствовал вашему URL-адресу маршрута. Ниже описаны реализации RouteBase и IRouteHandler.

Прежде чем вставить два класса, здесь очень простой пример использования. Обратите внимание, что эти реализации RouteBase и IRouteHandler действительно будут работать для IHttpHandlers, у которых даже нет файла .ashx, что довольно удобно.

// A "headless" IHttpHandler route (no .ashx file required)
RouteTable.Routes.Add(new GenericHandlerRoute<HeadlessService>("services/headless"));

Это приведет к тому, что все входящие URL-адреса, соответствующие маршруту "услуги/безглавой", будут переданы в новый экземпляр HeadlessService IHttpHandler (HeadlessService - это просто пример в этом случае. Это будет любая реализация IHttpHandler, которую вы хотели перейти к).

Итак, вот реализация классов маршрутизации, комментарии и все:

/// <summary>
/// For info on subclassing RouteBase, check Pro Asp.NET MVC Framework, page 252.
/// Google books link: http://books.google.com/books?id=tD3FfFcnJxYC&pg=PA251&lpg=PA251&dq=.net+RouteBase&source=bl&ots=IQhFwmGOVw&sig=0TgcFFgWyFRVpXgfGY1dIUc0VX4&hl=en&ei=z61UTMKwF4aWsgPHs7XbAg&sa=X&oi=book_result&ct=result&resnum=6&ved=0CC4Q6AEwBQ#v=onepage&q=.net%20RouteBase&f=false
/// 
/// It explains how the asp.net runtime will call GetRouteData() for every route in the route table.
/// GetRouteData() is used for inbound url matching, and should return null for a negative match (the current requests url doesn't match the route).
/// If it does match, it returns a RouteData object describing the handler that should be used for that request, along with any data values (stored in RouteData.Values) that
/// that handler might be interested in.
/// 
/// The book also explains that GetVirtualPath() (used for outbound url generation) is called for each route in the route table, but that is not my experience,
/// as mine used to simply throw a NotImplementedException, and that never caused a problem for me.  In my case, I don't need to do outbound url generation,
/// so I don't have to worry about it in any case.
/// </summary>
/// <typeparam name="T"></typeparam>
public class GenericHandlerRoute<T> : RouteBase where T : IHttpHandler, new()
{
    public string RouteUrl { get; set; }


    public GenericHandlerRoute(string routeUrl)
    {
        RouteUrl = routeUrl;
    }


    public override RouteData GetRouteData(HttpContextBase httpContext)
    {
        // See if the current request matches this route url
        string baseUrl = httpContext.Request.CurrentExecutionFilePath;
        int ix = baseUrl.IndexOf(RouteUrl);
        if (ix == -1)
            // Doesn't match this route.  Returning null indicates to the asp.net runtime that this route doesn't apply for the current request.
            return null;

        baseUrl = baseUrl.Substring(0, ix + RouteUrl.Length);

        // This is kind of a hack.  There no way to access the route data (or even the route url) from an IHttpHandler (which has a very basic interface).
        // We need to store the "base" url somewhere, including parts of the route url that are constant, like maybe the name of a method, etc.
        // For instance, if the route url "myService/myMethod/{myArg}", and the request url were "http://localhost/myApp/myService/myMethod/argValue",
        // the "current execution path" would include the "myServer/myMethod" as part of the url, which is incorrect (and it will prevent your UriTemplates from matching).
        // Since at this point in the exectuion, we know the route url, we can calculate the true base url (excluding all parts of the route url).
        // This means that any IHttpHandlers that use this routing mechanism will have to look for the "__baseUrl" item in the HttpContext.Current.Items bag.
        // TODO: Another way to solve this would be to create a subclass of IHttpHandler that has a BaseUrl property that can be set, and only let this route handler
        // work with instances of the subclass.  Perhaps I can just have RestHttpHandler have that property.  My reticence is that it would be nice to have a generic
        // route handler that works for any "plain ol" IHttpHandler (even though in this case, you have to use the "global" base url that stored in HttpContext.Current.Items...)
        // Oh well.  At least this works for now.
        httpContext.Items["__baseUrl"] = baseUrl;

        GenericHandlerRouteHandler<T> routeHandler = new GenericHandlerRouteHandler<T>();
        RouteData rdata = new RouteData(this, routeHandler);

        return rdata;
    }


    public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
    {
        // This route entry doesn't generate outbound Urls.
        return null;
    }
}



public class GenericHandlerRouteHandler<T> : IRouteHandler where T : IHttpHandler, new()
{
    public IHttpHandler GetHttpHandler(RequestContext requestContext)
    {
        return new T();
    }
}

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

Эти классы будут использоваться в моей предстоящей библиотеке С# REST, RestCake. Я надеюсь, что мой путь вниз по кроличьей лунке поможет любому другому, кто решает RouteBase, и делать классные вещи с помощью IHttpHandlers.

Ответ 2

Мне действительно нравится решение Joel, потому что вам не нужно знать тип обработчика, когда вы пытаетесь настроить маршруты. Я бы поддержал это, но, увы, мне не нужна репутация.

На самом деле я нашел решение, которое, по моему мнению, лучше, чем упомянутое. Исходный исходный код, полученный из моего примера, можно найти здесь http://weblogs.asp.net/leftslipper/archive/2009/10/07/introducing-smartyroute-a-smarty-ier-way-to-do-routing-in-asp-net-applications.aspx.

Это меньше кода, типа агностик и быстро.

public class HttpHandlerRoute : IRouteHandler {

  private String _VirtualPath = null;

  public HttpHandlerRoute(String virtualPath) {
    _VirtualPath = virtualPath;
  }

  public IHttpHandler GetHttpHandler(RequestContext requestContext) {
    IHttpHandler httpHandler = (IHttpHandler)BuildManager.CreateInstanceFromVirtualPath(_VirtualPath, typeof(IHttpHandler));
    return httpHandler;
  }
}

И пример использования

String handlerPath = "~/UploadHandler.ashx";
RouteTable.Routes.Add(new Route("files/upload", new HttpHandlerRoute(handlerPath)));

Ответ 3

EDIT: Я только что редактировал этот код, потому что у меня были некоторые проблемы со старым. Если вы используете старую версию, обновите ее.

Этот поток немного устарел, но я просто переписал часть кода здесь, чтобы сделать то же самое, но более элегантным способом, используя метод расширения.

Я использую это в ASP.NET Webforms, и мне нравится иметь файлы ashx в папке и иметь возможность вызывать их либо с помощью маршрутизации, либо с обычным запросом.

Итак, я довольно сильно схватил shellscape код и сделал метод расширения, который делает трюк. В конце я почувствовал, что также должен поддерживать передачу объекта IHttpHandler вместо его Url, поэтому я написал и перегрузил метод MapHttpHandlerRoute для этого.

namespace System.Web.Routing
{
 public class HttpHandlerRoute<T> : IRouteHandler where T: IHttpHandler
 {
  private String _virtualPath = null;

  public HttpHandlerRoute(String virtualPath)
  {
   _virtualPath = virtualPath;
  }

  public HttpHandlerRoute() { }

  public IHttpHandler GetHttpHandler(RequestContext requestContext)
  {
   return Activator.CreateInstance<T>();
  }
 }

 public class HttpHandlerRoute : IRouteHandler
 {
  private String _virtualPath = null;

  public HttpHandlerRoute(String virtualPath)
  {
   _virtualPath = virtualPath;
  }

  public IHttpHandler GetHttpHandler(RequestContext requestContext)
  {
   if (!string.IsNullOrEmpty(_virtualPath))
   {
    return (IHttpHandler)System.Web.Compilation.BuildManager.CreateInstanceFromVirtualPath(_virtualPath, typeof(IHttpHandler));
   }
   else
   {
    throw new InvalidOperationException("HttpHandlerRoute threw an error because the virtual path to the HttpHandler is null or empty.");
   }
  }
 }

 public static class RoutingExtension
 {
  public static void MapHttpHandlerRoute(this RouteCollection routes, string routeName, string routeUrl, string physicalFile, RouteValueDictionary defaults = null, RouteValueDictionary constraints = null)
  {
   var route = new Route(routeUrl, defaults, constraints, new HttpHandlerRoute(physicalFile));
   routes.Add(routeName, route);
  }

  public static void MapHttpHandlerRoute<T>(this RouteCollection routes, string routeName, string routeUrl, RouteValueDictionary defaults = null, RouteValueDictionary constraints = null) where T : IHttpHandler
  {
   var route = new Route(routeUrl, defaults, constraints, new HttpHandlerRoute<T>());
   routes.Add(routeName, route);
  }
 }
}

Я помещаю его в одно и то же пространство имен всех собственных объектов маршрутизации, поэтому он будет автоматически доступен.

Чтобы использовать это, вам просто нужно позвонить:

// using the handler url
routes.MapHttpHandlerRoute("DoSomething", "Handlers/DoSomething", "~/DoSomething.ashx");

или

// using the type of the handler
routes.MapHttpHandlerRoute<MyHttpHanler>("DoSomething", "Handlers/DoSomething");

Enjoy, Alex

Ответ 4

Да, я тоже это заметил. Возможно, есть встроенный способ ASP.NET для этого, но трюк для меня состоял в том, чтобы создать новый класс, полученный из IRouteHandler:
using System;
using System.IO;
using System.Reflection;
using System.Text.RegularExpressions;
using System.Web;
using System.Web.Routing;

namespace MyNamespace
{
    class GenericHandlerRouteHandler : IRouteHandler
    {
        private string _virtualPath;
        private Type _handlerType;
        private static object s_lock = new object();

        public GenericHandlerRouteHandler(string virtualPath)
        {
            _virtualPath = virtualPath;
        }

        #region IRouteHandler Members

        public System.Web.IHttpHandler GetHttpHandler(RequestContext requestContext)
        {
            ResolveHandler();

            IHttpHandler handler = (IHttpHandler)Activator.CreateInstance(_handlerType);
            return handler;
        }

        #endregion

        private void ResolveHandler()
        {
            if (_handlerType != null)
                return;

            lock (s_lock)
            {
                // determine physical path of ashx
                string path = _virtualPath.Replace("~/", HttpRuntime.AppDomainAppPath);

                if (!File.Exists(path))
                    throw new FileNotFoundException("Generic handler " + _virtualPath + " could not be found.");

                // parse the class name out of the .ashx file
                // unescaped reg-ex: (?<=Class=")[a-zA-Z\.]*
                string className;
                Regex regex = new Regex("(?<=Class=\")[a-zA-Z\\.]*");
                using (var sr = new StreamReader(path))
                {
                    string str = sr.ReadToEnd();

                    Match match = regex.Match(str);
                    if (match == null)
                        throw new InvalidDataException("Could not determine class name for generic handler " + _virtualPath);

                    className = match.Value;
                }

                // get the class type from the name
                Assembly[] asms = AppDomain.CurrentDomain.GetAssemblies();
                foreach (Assembly asm in asms)
                {
                    _handlerType = asm.GetType(className);
                    if (_handlerType != null)
                        break;
                }

                if (_handlerType == null)
                    throw new InvalidDataException("Could not find type " + className + " in any loaded assemblies.");
            }
        }
    }
}

Чтобы создать маршрут для .ashx:

IRouteHandler routeHandler = new GenericHandlerRouteHandler("~/somehandler.ashx");
Route route = new Route("myroute", null, null, null, routeHandler);
RouteTable.Routes.Add(route);

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

Ответ 5

Все эти ответы очень хороши. Мне нравится простота класса мишама GenericHandlerRouteHandler<T>. Это отличная идея устранить ненужную ссылку на виртуальный путь, если вы знаете конкретный класс HttpHandler. Однако класс GenericHandlerRoute<T> не нужен. Существующий класс Route, полученный из RouteBase, уже обрабатывает всю сложность согласования маршрутов, параметры и т.д., Поэтому мы можем использовать его вместе с GenericHandlerRouteHandler<T>.

Ниже приведена комбинированная версия с реальным примером использования, включающим параметры маршрута.

Сначала идут обработчики маршрутов. Есть два включенных здесь - оба с тем же именем класса, но один, который является общим и использует информацию о типе, чтобы создать экземпляр конкретного HttpHandler, как в использовании мистера Мишама, и тот, который использует виртуальный путь и BuildManager, чтобы создать экземпляр соответствующего HttpHandler, как в использовании shellscape. Хорошая новость заключается в том, что .NET позволяет жить бок о бок просто отлично, поэтому мы можем просто использовать то, что хотим, и можем переключаться между ними по своему желанию.

using System.Web;
using System.Web.Compilation;
using System.Web.Routing;

public class HttpHandlerRouteHandler<T> : IRouteHandler where T : IHttpHandler, new() {

  public HttpHandlerRouteHandler() { }

  public IHttpHandler GetHttpHandler(RequestContext requestContext) {
    return new T();
  }
}

public class HttpHandlerRouteHandler : IRouteHandler {

  private string _VirtualPath;

  public HttpHandlerRouteHandler(string virtualPath) {
    this._VirtualPath = virtualPath;
  }

  public IHttpHandler GetHttpHandler(RequestContext requestContext) {
    return (IHttpHandler) BuildManager.CreateInstanceFromVirtualPath(this._VirtualPath, typeof(IHttpHandler));
  }

}

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

Ниже показана регистрация маршрута, используемого для достижения этого, с помощью DocumentHandler HttpHandler:

routes.Add("Document", new Route("document/{documentId}/{*fileName}", new HttpHandlerRouteHandler<DocumentHandler>()));

Я использовал {*fileName}, а не только {fileName}, чтобы параметр fileName выступал в качестве необязательного параметра catch-all.

Чтобы создать URL-адрес для файла, обслуживаемого этим HttpHandler, мы можем добавить следующий статический метод в класс, где такой метод будет подходящим, например, в классе HttpHandler:

public static string GetFileUrl(int documentId, string fileName) {
  string mimeType = null;
  try { mimeType = MimeMap.GetMimeType(Path.GetExtension(fileName)); }
  catch { }
  RouteValueDictionary documentRouteParameters = new RouteValueDictionary {   { "documentId", documentId.ToString(CultureInfo.InvariantCulture) }
                                                                            , { "fileName",   DocumentHandler.IsPassThruMimeType(mimeType) ? fileName : string.Empty } };
  return RouteTable.Routes.GetVirtualPath(null, "Document", documentRouteParameters).VirtualPath;
}

Я пропустил определения MimeMap и и IsPassThruMimeType, чтобы этот пример был простым. Но они предназначены для определения того, должны ли конкретные типы файлов предоставлять свои имена файлов непосредственно в URL-адресе, или, скорее, в заголовке Content-Disposition HTTP. Некоторые расширения файлов могут быть заблокированы IIS или сканированием URL или могут привести к выполнению кода, что может вызвать проблемы для пользователей - особенно если источником файла является другой пользователь, который является злонамеренным. Вы можете заменить эту логику на другую логику фильтрации или полностью исключить такую ​​логику, если вы не подвергаетесь этому типу риска.

Так как в этом конкретном примере имя файла может быть опущено из URL-адреса, то, очевидно, мы должны получить имя файла откуда-то. В этом конкретном примере имя файла может быть восстановлено путем выполнения поиска с использованием идентификатора документа и включения имени файла в URL-адрес, предназначенный исключительно для улучшения работы пользователя. Таким образом, DocumentHandler HttpHandler может определить, было ли указано имя файла в URL-адресе, а если это не так, то он может просто добавить HTTP-заголовок Content-Disposition в ответ.

Оставаясь в теме, важной частью вышеуказанного кодового блока является использование RouteTable.Routes.GetVirtualPath() и параметры маршрутизации для создания URL-адреса из объекта Route, который мы создали во время регистрации маршрута процесс.

Здесь приведена утопленная версия класса DocumentHandler HttpHandler (для ясности она значительно опущена). Вы можете видеть, что этот класс использует параметры маршрута для получения идентификатора документа и имени файла, когда это возможно; в противном случае он попытается извлечь идентификатор документа из параметра строки запроса (т.е. при условии, что маршрутизация не используется).

public void ProcessRequest(HttpContext context) {

  try {

    context.Response.Clear();

    // Get the requested document ID from routing data, if routed.  Otherwise, use the query string.
    bool    isRouted    = false;
    int?    documentId  = null;
    string  fileName    = null;
    RequestContext requestContext = context.Request.RequestContext;
    if (requestContext != null && requestContext.RouteData != null) {
      documentId  = Utility.ParseInt32(requestContext.RouteData.Values["documentId"] as string);
      fileName    = Utility.Trim(requestContext.RouteData.Values["fileName"] as string);
      isRouted    = documentId.HasValue;
    }

    // Try the query string if no documentId obtained from route parameters.
    if (!isRouted) {
      documentId  = Utility.ParseInt32(context.Request.QueryString["id"]);
      fileName    = null;
    }
    if (!documentId.HasValue) { // Bad request
      // Response logic for bad request omitted for sake of simplicity
      return;
    }

    DocumentDetails documentInfo = ... // Details of loading this information omitted

    if (context.Response.IsClientConnected) {

      string fileExtension = string.Empty;
      try { fileExtension = Path.GetExtension(fileName ?? documentInfo.FileName); } // Use file name provided in URL, if provided, to get the extension.
      catch { }

      // Transmit the file to the client.
      FileInfo file = new FileInfo(documentInfo.StoragePath);
      using (FileStream fileStream = file.OpenRead()) {

        // If the file size exceeds the threshold specified in the system settings, then we will send the file to the client in chunks.
        bool mustChunk = fileStream.Length > Math.Max(SystemSettings.Default.MaxBufferedDownloadSize * 1024, DocumentHandler.SecondaryBufferSize);

        // WARNING! Do not ever set the following property to false!
        //          Doing so causes each chunk sent by IIS to be of the same size,
        //          even if a chunk you are writing, such as the final chunk, may
        //          be shorter than the rest, causing extra bytes to be written to
        //          the stream.
        context.Response.BufferOutput   = true;

        context.Response.ContentType = MimeMap.GetMimeType(fileExtension);
        context.Response.AddHeader("Content-Length", fileStream.Length.ToString(CultureInfo.InvariantCulture));
        if (   !isRouted
            || string.IsNullOrWhiteSpace(fileName)
            || string.IsNullOrWhiteSpace(fileExtension)) {  // If routed and a file name was provided in the route, then the URL will appear to point directly to a file, and no file name header is needed; otherwise, add the header.
          context.Response.AddHeader("Content-Disposition", string.Format("attachment; filename={0}", HttpUtility.UrlEncode(documentInfo.FileName)));
        }

        int     bufferSize      = DocumentHandler.SecondaryBufferSize;
        byte[]  buffer          = new byte[bufferSize];
        int     bytesRead       = 0;

        while ((bytesRead = fileStream.Read(buffer, 0, bufferSize)) > 0 && context.Response.IsClientConnected) {
          context.Response.OutputStream.Write(buffer, 0, bytesRead);
          if (mustChunk) {
            context.Response.Flush();
          }
        }
      }

    }

  }
  catch (Exception e) {
    // Error handling omitted from this example.
  }
}

В этом примере используются некоторые дополнительные пользовательские классы, например класс Utility, чтобы упростить некоторые тривиальные задачи. Но, надеюсь, вы сможете справиться с этим. Единственной действительно важной частью этого класса в отношении текущей темы, конечно же, является поиск параметров маршрута из context.Request.RequestContext.RouteData. Но я видел несколько сообщений в других разделах о том, как передавать большие файлы с помощью HttpHandler без пережевывания памяти сервера, поэтому было бы неплохо объединить примеры.