28

昔ながらIHttpHandlerのを使用したRESTサービスがいくつかあります。パスに.ashxが含まれないように、よりクリーンなURLを生成したいと思います。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)。 サービス用とMVC用のSystem.Web.Routing.RouteBase2つの派生クラスしかないようです。何が行われるのかわかりません(Reflectorはメソッド本体を表示せず、「NGen画像の境界を越えてこのタイプのメソッドをインライン化するために重要なパフォーマンス」を表示するだけです)。ServiceRouteDynamicDataRouteMapPageRoute()

私はそれRouteBaseが封印されておらず、比較的単純なインターフェースを持っていることがわかります:

public abstract RouteData GetRouteData(HttpContextBase httpContext);

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

したがって、おそらく私は自分のHttpHandlerRouteを作成できます。私はそれを試してみますが、ルートをIHttpHandlerにマッピングする既存の方法または組み込みの方法を誰かが知っているなら、それは素晴らしいことです。

4

5 に答える 5

27

わかりました、私は最初に質問をして以来、これを理解しており、最終的に私が望むことだけを行うソリューションを手に入れました. ただし、少し前もって説明する必要があります。IHttpHandler は非常に基本的なインターフェイスです。

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

ルート データにアクセスするための組み込みプロパティはなく、ルート データもコンテキストまたは要求で見つけることができません。System.Web.UI.PageオブジェクトにはRouteDataプロパティがServiceRouteあり、UriTemplates を解釈し、値を正しいメソッドに内部的に渡すすべての作業を行います。ASP.NET MVC は、ルート データにアクセスする独自の方法を提供します。持っていたとしてもRouteBase(a) 着信 URL がルートに一致するかどうかを判断し、(b) IHttpHandler 内から使用される個々の値をすべて抽出するために URL を解析した場合、そのルート データを IHttpHandler に渡す簡単な方法はありません。IHttpHandler を「純粋」に保ちたい場合は、いわば、URL を処理し、そこから値を抽出する方法を担当します。この場合の RouteBase 実装は、IHttpHandler を使用する必要があるかどうかを判断するためにのみ使用されます。

ただし、1 つの問題が残ります。着信 URL がルートに一致すると RouteBase が判断すると、リクエストを処理する IHttpHandler のインスタンスを作成する IRouteHandler に渡されます。しかし、いったん IHttpHandler に入ると、 の値context.Request.CurrentExecutionFilePathは誤解を招きます。これは、クライアントから取得した URL からクエリ文字列を除いたものです。したがって、.ashx ファイルへのパスではありません。また、一定のルートの部分 (メソッドの名前など) は、その実行ファイル パス値の一部になります。IHttpHandler 内で UriTemplates を使用して、IHttpHandler 内のどの特定のメソッドが要求を処理する必要があるかを判断する場合、これは問題になる可能性があります。

例: /myApp/services/myHelloWorldHandler.ashx に .ashx ハンドラーがあり、ハンドラーにマップされた次のルートがある場合: "services/hello/{name}" そして、この URL に移動して、SayHello(string name)メソッドを呼び出そうとしました。あなたのハンドラの: http://localhost/myApp/services/hello/SayHello/Sam

/ myApp CurrentExecutionFilePath/services/hello/Sam. ルート URL の一部が含まれており、これが問題です。実行ファイルのパスをルート URL と一致させる必要があります。以下の実装はRouteBaseIRouteHandlerこの問題に対処しています。

2 つのクラスを貼り付ける前に、非常に簡単な使用例を示します。RouteBase と IRouteHandler のこれらの実装は、.ashx ファイルさえも持たない IHttpHandlers に対して実際に機能することに注意してください。これは非常に便利です。

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

これにより、「services/headless」ルートに一致するすべての受信 URL がHeadlessServiceIHttpHandler の新しいインスタンスに渡されます (この場合、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's 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's 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's 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 のどの部分がルートに属しているか、どの部分がサービス呼び出しの実際の引数であるかを適切に判断できるようにしました。

これらのクラスは、今後の C# REST ライブラリであるRestCakeで使用されます。ルーティングのうさぎの穴を下る私の道筋が、RouteBase を使用することを決定し、IHttpHandlers でクールなことを行うことを決定した他の人の助けになることを願っています。

于 2010-08-01T00:06:53.997 に答える
13
于 2010-08-11T04:25:16.373 に答える
12

編集: 古いコードに問題があったため、このコードを編集しました。古いバージョンを使用している場合は、アップデートしてください。

このスレッドは少し古いですが、拡張メソッドを使用して、同じことをより洗練された方法で行うために、ここのコードの一部を書き直しました。

私はこれを ASP.net Webforms で使用しています。ashx ファイルをフォルダーに置き、ルーティングまたは通常の要求を使用してそれらを呼び出すことができるようにしたいと考えています。

それで、私は shellscape のコードをほとんど手に入れて、そのトリックを行う拡張メソッドを作成しました。最後に、Url の代わりに IHttpHandler オブジェクトを渡すこともサポートする必要があると感じたので、そのための 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");

楽しんでね、アレックス

于 2011-10-20T23:49:58.227 に答える
5

これらの答えはすべて非常に優れています。GenericHandlerRouteHandler<T>ミーチャム先生の授業のシンプルさが大好きです。HttpHandler特定のクラスがわかっている場合は、仮想パスへの不要な参照を削除することをお勧めします。ただし、GenericHandlerRoute<T>クラスは必要ありません。Routeから派生する既存のクラスRouteBaseは、ルート マッチング、パラメータなどの複雑さをすべて処理しているため、 と一緒に使用することができますGenericHandlerRouteHandler<T>

以下は、ルートパラメーターを含む実際の使用例と組み合わせたバージョンです。

最初はルート ハンドラです。ここには 2 つ含まれています。どちらも同じクラス名ですが、1 つはジェネリックであり、型情報を使用してHttpHandlerMeacham 氏の使用法のように特定のインスタンスを作成し、もう 1 つは仮想パスを使用しBuildManagerてインスタンスを作成します。HttpHandlershellscape の使用法と同様に適切です。幸いなことに、.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、提供するドキュメントを検索するためのドキュメント ID を期待する場合があり、ブラウザに提供するファイル名 (サーバーで使用されるファイル名とは異なる場合がある) を期待する場合があります。

以下は、 でこれを達成するために使用されるルートの登録を示していますDocumentHandler HttpHandler

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

パラメータがオプションのキャッチオール パラメータとして機能する ことを許可する{*fileName}だけでなく、使用しました。{fileName}fileName

この によって提供されるファイルの 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、 and との定義を省略しました。ただし、これらは、特定のファイルの種類がファイル名を URL で直接提供する必要があるかどうか、またはHTTP ヘッダーIsPassThruMimeTypeで提供する必要があるかどうかを判断することを目的としています。Content-Disposition一部のファイル拡張子は、IIS または URL スキャンによってブロックされるか、ユーザーに問題を引き起こす可能性のあるコードが実行される可能性があります (特に、ファイルのソースが悪意のある別のユーザーである場合)。このタイプのリスクにさらされていない場合は、このロジックを他のフィルタリング ロジックに置き換えるか、そのようなロジックを完全に省略することができます。

この特定の例では、ファイル名が URL から省略されている可能性があるため、明らかに、どこかからファイル名を取得する必要があります。この特定の例では、ドキュメント ID を使用してルックアップを実行することでファイル名を取得できます。URL にファイル名を含めることは、ユーザー エクスペリエンスを向上させることのみを目的としています。そのため、DocumentHandler HttpHandlerは URL にファイル名が指定されているかどうかを判別できます。指定されていない場合は、単純にContent-DispositionHTTP ヘッダーを応答に追加できます。

トピックにとどまり、上記のコード ブロックの重要な部分は、ルート登録プロセス中に作成したオブジェクトRouteTable.Routes.GetVirtualPath()から URL を生成するためのルーティング パラメータの使用です。Route

これは、クラスの簡素化されたバージョンですDocumentHandler HttpHandler(わかりやすくするために、かなり省略されています)。このクラスはルート パラメータを使用して、可能な場合はドキュメント ID とファイル名を取得していることがわかります。それ以外の場合は、クエリ文字列パラメーターからドキュメント ID を取得しようとします (つまり、ルーティングが使用されていないと仮定します)。

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ので、例を組み合わせるのは良い考えのように思えました。

于 2010-12-04T09:53:47.433 に答える
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);

上記のコードは、ルート引数で動作するように拡張する必要があるかもしれませんが、それは出発点です。コメント歓迎。

于 2010-07-30T15:26:32.613 に答える