ASP.NET MVCでServer.Transferをシミュレートする方法は?


124

ASP.NET MVCでは、リダイレクトActionResultを非常に簡単に返すことができます。

 return RedirectToAction("Index");

 or

 return RedirectToRoute(new { controller = "home", version = Math.Random() * 10 });

これは実際には通常は問題ないHTTPリダイレクトを提供します。ただし、Googleアナリティクスを使用すると、元のリファラーが失われ、Googleがどこから来たのかわからなくなるため、大きな問題が発生します。これにより、検索エンジンの用語などの有用な情報が失われます。

ちなみに、この方法には、キャンペーンから取得された可能性のあるパラメーターを削除できるという利点がありますが、サーバー側でそれらをキャプチャすることができます。クエリ文字列にそれらを残すと、ブックマークやTwitterやブログにリンクしてはいけないリンクが表示されます。キャンペーンIDを含む私たちのサイトへのリンクがTwitterで人々に送られているのを何度か見たことがあります。

とにかく、私は別の場所や別のバージョンにリダイレクトする可能性があるサイトへのすべての着信訪問のための「ゲートウェイ」コントローラーを書いています。

今のところ、私は(偶然のブックマークよりも)今のところGoogleのことをもっと気にかけており、にアクセス/した場合に表示されるページにアクセスしたユーザー/home/7に、ホームページのバージョン7 を送信できるようにしたいと考えています。

前に言ったように、これを行うと、Googleがリファラーを分析できなくなります。

 return RedirectToAction(new { controller = "home", version = 7 });

私が本当に欲しいのは

 return ServerTransferAction(new { controller = "home", version = 7 });

これにより、クライアント側のリダイレクトなしでそのビューが表示されます。そんなことあるとは思いませんが。

現在私が思いつくことができる最良のことはHomeController.Index(..)、私のGatewayController.Indexアクションでコントローラロジック全体を複製することです。つまり、アクセスしやすいように移動'Views/Home'する'Shared'必要がありました。もっと良い方法があるに違いない?? ..


正確に何ServerTransferActionを複製しようとしていましたか?それは本当ですか?(それに関する情報を見つけることができませんでした...質問に感謝します、ところで、以下の答えは素晴らしいです)
jleach

Server.Transfer(...)を検索します。これは基本的にサーバー側で「リダイレクト」を行う方法であり、クライアントはクライアント側のリダイレクトなしでリダイレクトされたページを受信します。通常、最新のルーティングでは推奨されません。
Simon_Weaver 2016年

1
「転送」は、古くなったASP.NETの機能であり、ルーティングを使用して正しいコントローラアクションに直接移動できるため、MVCでは不要になりました。詳細については、この回答を参照してください。
NightOwl888 2018年

@ NightOwl888はい、間違いなくそうです。しかし、ビジネスロジックのために、必要/簡単な場合もあります。私はこれをどのように使用したのかを振り返りました-(幸いにも1か所だけでした)-特定の複雑な状況で動的になりたいホームページがあるため、舞台裏では別のルートが表示されています。ルーティングまたはルート条件を優先して、できる限り回避したいのは確かですが、単純なifステートメントでは解決策が魅力的すぎる場合があります。
Simon_Weaver

@Simon_Weaver-そして、サブクラス化の何が問題になっているRouteBaseのでif、あるコントローラーから別のコントローラーにジャンプするためにすべてを逆方向に曲げる代わりに、ステートメントをそこに置くことができますか?
NightOwl888

回答:


130

TransferResultクラスはどうですか?(スタンス回答に基づく)

/// <summary>
/// Transfers execution to the supplied url.
/// </summary>
public class TransferResult : ActionResult
{
    public string Url { get; private set; }

    public TransferResult(string url)
    {
        this.Url = url;
    }

    public override void ExecuteResult(ControllerContext context)
    {
        if (context == null)
            throw new ArgumentNullException("context");

        var httpContext = HttpContext.Current;

        // MVC 3 running on IIS 7+
        if (HttpRuntime.UsingIntegratedPipeline)
        {
            httpContext.Server.TransferRequest(this.Url, true);
        }
        else
        {
            // Pre MVC 3
            httpContext.RewritePath(this.Url, false);

            IHttpHandler httpHandler = new MvcHttpHandler();
            httpHandler.ProcessRequest(httpContext);
        }
    }
}

更新: MVC3で動作するようになりました(Simonの投稿のコードを使用)。する必要があり、それはIIS7 +の統合パイプライン内で実行されますかどうかを調べることでもMVC2で仕事を(それをテストすることができていません)。

完全な透明性のため; 本番環境では、TransferResultを直接使用することはありません。TransferToRouteResultを使用して、呼び出しがTransferResultを実行します。これが、実際に運用サーバーで実行されているものです。

public class TransferToRouteResult : ActionResult
{
    public string RouteName { get;set; }
    public RouteValueDictionary RouteValues { get; set; }

    public TransferToRouteResult(RouteValueDictionary routeValues)
        : this(null, routeValues)
    {
    }

    public TransferToRouteResult(string routeName, RouteValueDictionary routeValues)
    {
        this.RouteName = routeName ?? string.Empty;
        this.RouteValues = routeValues ?? new RouteValueDictionary();
    }

    public override void ExecuteResult(ControllerContext context)
    {
        if (context == null)
            throw new ArgumentNullException("context");

        var urlHelper = new UrlHelper(context.RequestContext);
        var url = urlHelper.RouteUrl(this.RouteName, this.RouteValues);

        var actualResult = new TransferResult(url);
        actualResult.ExecuteResult(context);
    }
}

また、T4MVCを使用している場合(使用していない場合は...使用してください)、この拡張機能が役立つ場合があります。

public static class ControllerExtensions
{
    public static TransferToRouteResult TransferToAction(this Controller controller, ActionResult result)
    {
        return new TransferToRouteResult(result.GetRouteValueDictionary());
    }
}

あなたができるこの小さな宝石を使用して

// in an action method
TransferToAction(MVC.Error.Index());

1
これはうまくいきます。無限ループに陥らないように注意してください。間違ったURLを渡して最初の試行を行ったときのように、小さな値を変更して、他の人に役立つルート値コレクションを渡せるようにしました。上または下に投稿...
Simon_Weaver 2009

更新:このソリューションはうまく機能しているようですが、非常に限られた容量でのみ使用していますが、問題はまだ発見されていません
Simon_Weaver

1つの問題:POSTからGETリクエストにリダイレクトできない-しかし、それは必ずしも悪いことではありません。ただし注意が必要なこと
Simon_Weaver '11 / 11/11

2
@BradLaney: 'var urlHelper ...'と 'var url ...'の行を削除し、残りの部分では 'url'を 'this.Url'に置き換えるだけで機能します。:)
マイケルウルマン、2011

1
1:カップリング/単体テスト/将来の互換性。2:mvcコア/ mvcサンプルは、このシングルトンを使用しません。3:このシングルトンは、非同期アクションメソッドの使用時など、スレッド(null)、プールスレッド、またはデフォルト以外のコンテキストで呼び出された非同期デリゲートのいずれかでは使用できません。4:互換性のみを目的として、mvcはユーザーコードを入力する前にこのシングルトン値をcontext.HttpContextに設定します。
Softlion 2013年

47

編集:ASP.NET MVC 3と互換性があるように更新されました

IIS7を使用している場合、ASP.NET MVC 3では次の変更が機能するようです。@ nitinと@andyのおかげで、元のコードが機能しなかったことを指摘しました。

2011年4月11日の編集:MVC 3 RTMの時点で、Server.TransferRequestでTempDataが壊れる

以下のコードを変更して例外をスローしますが、現時点では他の解決策はありません。


Markusが修正したStanの元の投稿を修正したものを以下に示します。Route Valueディクショナリを取得するために追加のコンストラクターを追加し、それが単なるリダイレクトである可能性があるという混乱を避けるためにMVCTransferResultに名前を変更しました。

これで、リダイレクトに対して次のことができます。

return new MVCTransferResult(new {controller = "home", action = "something" });

私の変更されたクラス:

public class MVCTransferResult : RedirectResult
{
    public MVCTransferResult(string url)
        : base(url)
    {
    }

    public MVCTransferResult(object routeValues):base(GetRouteURL(routeValues))
    {
    }

    private static string GetRouteURL(object routeValues)
    {
        UrlHelper url = new UrlHelper(new RequestContext(new HttpContextWrapper(HttpContext.Current), new RouteData()), RouteTable.Routes);
        return url.RouteUrl(routeValues);
    }

    public override void ExecuteResult(ControllerContext context)
    {
        var httpContext = HttpContext.Current;

        // ASP.NET MVC 3.0
        if (context.Controller.TempData != null && 
            context.Controller.TempData.Count() > 0)
        {
            throw new ApplicationException("TempData won't work with Server.TransferRequest!");
        }

        httpContext.Server.TransferRequest(Url, true); // change to false to pass query string parameters if you have already processed them

        // ASP.NET MVC 2.0
        //httpContext.RewritePath(Url, false);
        //IHttpHandler httpHandler = new MvcHttpHandler();
        //httpHandler.ProcessRequest(HttpContext.Current);
    }
}

1
これはMVC 3 RCでは機能していないようです。HttpHandler.ProcessRequest()で失敗します、「HttpContext.SetSessionStateBehavior」は「HttpApplication.AcquireRequestState」イベントが発生する前にのみ呼び出すことができます。
アンディ

MVC3を見る変更はまだありません。解決策を見つけた場合はお知らせください
Simon_Weaver

Nitinによって提案されたServer.TransferRquestは、上記が試みていることを実行しますか?
Old Geezer

TempDataのnullを確認してカウント> 0にする必要があるのはなぜですか?
yurart 2017

そうではありませんが、これは単なる安全機能なので、すでに使用していて
信頼している


12

ASP.NET MVCがServer.Transfer()をサポートしていないことを最近知り、スタブメソッド(Default.aspx.csに触発された)を作成しました。

    private void Transfer(string url)
    {
        // Create URI builder
        var uriBuilder = new UriBuilder(Request.Url.Scheme, Request.Url.Host, Request.Url.Port, Request.ApplicationPath);
        // Add destination URI
        uriBuilder.Path += url;
        // Because UriBuilder escapes URI decode before passing as an argument
        string path = Server.UrlDecode(uriBuilder.Uri.PathAndQuery);
        // Rewrite path
        HttpContext.Current.RewritePath(path, false);
        IHttpHandler httpHandler = new MvcHttpHandler();
        // Process request
        httpHandler.ProcessRequest(HttpContext.Current);
    }

9

リダイレクトしたいコントローラーのインスタンスを作成し、必要なアクションメソッドを呼び出して、その結果を返すだけではありませんか?何かのようなもの:

 HomeController controller = new HomeController();
 return controller.Index();

4
いいえ、作成したコントローラーには、リクエストとレスポンスなどが正しく設定されていません。これは問題を引き起こす可能性があります。
Jeff Walkerコードレンジャー2014年

私は@JeffWalkerCodeRangerに同意します。プロパティを設定した後も同じですotherController.ControllerContext = this.ControllerContext;
T-moty

7

現在の要求を別のコントローラー/アクションにルーティングし直しながら、2番目のコントローラー/アクションが要求された場合とまったく同じ実行パスを維持したいと考えました。私の場合、さらにデータを追加したかったため、Server.Requestは機能しませんでした。これは、実際には、別のHTTP GET / POSTを実行し、結果をクライアントにストリーミングする現在のハンドラと同等です。これを達成するためのより良い方法があると確信していますが、これが私にとってうまくいくものです:

RouteData routeData = new RouteData();
routeData.Values.Add("controller", "Public");
routeData.Values.Add("action", "ErrorInternal");
routeData.Values.Add("Exception", filterContext.Exception);

var context = new HttpContextWrapper(System.Web.HttpContext.Current);
var request = new RequestContext(context, routeData);

IController controller = ControllerBuilder.Current.GetControllerFactory().CreateController(filterContext.RequestContext, "Public");
controller.Execute(request);

あなたの推測は正しいです:私はこのコードを入れました

public class RedirectOnErrorAttribute : ActionFilterAttribute, IExceptionFilter

本番環境では通常のリダイレクトを使用しますが、開発者にエラーを表示するために使用しています。リクエスト間で例外データを渡すために、ASP.NETセッション、データベース、または他のいくつかの方法を使用したくないことに注意してください。


7

MVCはサーバー転送をシミュレートするのではなく、実際にServer.TransferRequestを実行できます。

public ActionResult Whatever()
{
    string url = //...
    Request.RequestContext.HttpContext.Server.TransferRequest(url);
    return Content("success");//Doesn't actually get returned
}

さらに説明するために、回答にテキストを自由に追加してください。
Wladimir Palant 2014年

これにはMVCv3以上が必要です。
Sep

5

他のコントローラーをインスタンス化し、そのアクションメソッドを実行するだけです。


これにより、アドレスバーに目的のURLが表示されません
arserbin3 '29

@ arserbin3-Server.Transferもしません。この要件が、元の質問が投稿された理由であると考えられます。
Richard Szalay、2014年

2

他のコントローラーを新しくして、結果を返すアクションメソッドを呼び出すことができます。ただし、ビューを共有フォルダーに配置する必要があります。

これがあなたが複製によって意味したものであるかどうかはわかりませんが:

return new HomeController().Index();

編集する

別のオプションとして、独自のControllerFactoryを作成することもできます。これにより、作成するコントローラーを決定できます。


これはアプローチかもしれませんが、私がhc.ControllerContext = this.ControllerContextと言ったとしても、それはコンテキストが正しくないようです。さらに、〜/ Views / Gateway / 5.aspxでビューを探しますが、見つかりません。
Simon_Weaver

さらに、すべてのアクションフィルターが失われます。コントローラーが実装する必要のあるIControllerインターフェイスでExecuteメソッドを使用してみてください。例:((IController)new HomeController())。Execute(...)このようにして、Action Invokerパイプラインに参加します。Executeに渡す内容を正確に把握する必要があります... Reflectorが役立つかもしれません:)
Andrew Stanton-Nurse

はい、コントローラーを新しくするという考えは好きではありません。適切な拡張ポイントのように見える独自のコントローラーファクトリを定義する方がいいと思います。しかし、私はこのフレームワークの表面をかろうじて引っ掻いただけなので、私は途方に暮れるかもしれません。
JoshBerke 2009

1

ルーティングはこのシナリオを処理するだけではありませんか?つまり、上記のシナリオでは、このロジックを実装するルートハンドラーを作成するだけで済みます。


プログラム的な条件に基づいています。つまり、キャンペーン100はビュー7に移動し、キャンペーン200はビュー8に移動するなど、ルーティングするには複雑すぎる
Simon_Weaver 2009

4
ルーティングが複雑すぎるのはなぜですか?カスタムルート制約の何が問題になっていますか?stephenwalther.com/blog/archive/2008/08/07/...
イアン・マーサー

1

上記のTransferResultクラスのみを使用して、式ベースのルーティングを使用するユーザーのために、TrickDataをトリックして保持するコントローラー拡張メソッドを次に示します。TransferToRouteResultは必要ありません。

public static ActionResult TransferRequest<T>(this Controller controller, Expression<Action<T>> action)
    where T : Controller
{
     controller.TempData.Keep();
     controller.TempData.Save(controller.ControllerContext, controller.TempDataProvider);
     var url = LinkBuilder.BuildUrlFromExpression(controller.Request.RequestContext, RouteTable.Routes, action);
     return new TransferResult(url);
}

警告:これは実際には機能していますが、「SessionStateTempDataProviderクラスではセッション状態を有効にする必要があります」というエラーが発生するようです。このエラーはログにのみ表示されます。エラーロギングにELMAHを使用していて、InProcとAppFabricでこのエラーが発生します
Simon_Weaver

1

Server.TransferRequestあるMVCで完全に不要。これは時代遅れの機能で、ASP.NETでのみ必要でした。これは、要求が直接ページに到達し、要求を別のページに転送する方法が必要だったためです。ASP.NETの最新バージョン(MVCを含む)には、ルーティングするようにカスタマイズできるルーティングインフラストラクチャがあります。必要なリソースに直接があります。リクエストを直接コントローラーに送り、必要なアクションを実行できる場合は、リクエストをコントローラーに到達させて別のコントローラーに転送するだけの意味はありません。

さらに、元のリクエストに応答しているので、何も入力する必要がありません。TempData、リクエストを適切な場所にルーティングするためだけに、他のストレージに。代わりに、元のリクエストをそのままにしてコントローラーアクションに到達します。このアプローチは完全にサーバー側で行われるため、Googleがこのアプローチを承認するので安心できます。

IRouteConstraintとの両方からかなりのことができIRouteHandlerますが、ルーティングの最も強力な拡張ポイントはRouteBaseサブクラスです。このクラスは、着信ルートと発信URL生成の両方を提供するように拡張できます。これにより、このクラスは、URLとURLが実行するアクションに関係するすべてのことをワンストップで実行できます。

したがって、2番目の例に従って、からに移動/する/home/7には、適切なルート値を追加するルートが必要です。

public class RouteConfig
{
    public static void RegisterRoutes(RouteCollection routes)
    {
        routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

        // Routes directy to `/home/7`
        routes.MapRoute(
            name: "Home7",
            url: "",
            defaults: new { controller = "Home", action = "Index", version = 7 }
        );

        routes.MapRoute(
            name: "Default",
            url: "{controller}/{action}/{id}",
            defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
        );
    }
}

ただし、ランダムなページがある元の例に戻ると、ルートパラメータは実行時に変更できないため、より複雑です。したがって、RouteBase次のようにサブクラスで実行できます。

public class RandomHomePageRoute : RouteBase
{
    private Random random = new Random();

    public override RouteData GetRouteData(HttpContextBase httpContext)
    {
        RouteData result = null;

        // Only handle the home page route
        if (httpContext.Request.Path == "/")
        {
            result = new RouteData(this, new MvcRouteHandler());

            result.Values["controller"] = "Home";
            result.Values["action"] = "Index";
            result.Values["version"] = random.Next(10) + 1; // Picks a random number from 1 to 10
        }

        // If this isn't the home page route, this should return null
        // which instructs routing to try the next route in the route table.
        return result;
    }

    public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
    {
        var controller = Convert.ToString(values["controller"]);
        var action = Convert.ToString(values["action"]);

        if (controller.Equals("Home", StringComparison.OrdinalIgnoreCase) &&
            action.Equals("Index", StringComparison.OrdinalIgnoreCase))
        {
            // Route to the Home page URL
            return new VirtualPathData(this, "");
        }

        return null;
    }
}

次のようにルーティングに登録できます:

public class RouteConfig
{
    public static void RegisterRoutes(RouteCollection routes)
    {
        routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

        // Routes to /home/{version} where version is randomly from 1-10
        routes.Add(new RandomHomePageRoute());

        routes.MapRoute(
            name: "Default",
            url: "{controller}/{action}/{id}",
            defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
        );
    }
}

上記の例では、ユーザーがアクセスしたホームページのバージョンを記録するCookieを保存して、ユーザーが戻ったときに同じホームページのバージョンを受け取ることも意味があることに注意してください。

また、このアプローチを使用すると、ルーティングをカスタマイズしてクエリ文字列パラメーターを考慮し(デフォルトでは完全に無視されます)、それに応じて適切なコントローラーアクションにルーティングできます。

追加の例


アクションの入力時にすぐに転送するのではなく、そのアクションに何らかの作業をさせてから、条件付きで別のアクションに転送する場合はどうでしょうか。転送先に直接移動するようにルーティングを変更しても機能しないためServer.TransferRequest、結局のところ、「MVCでは完全に不要」ではないようです。
ProfK

0

それ自体は答えではありませんが、明らかに、実際のナビゲーションがWebforms Server.Transfer()の同等の機能を「実行」するためだけでなく、ユニットテスト内でこれらすべてが完全にサポートされることも必要です。

したがって、ServerTransferResultはRedirectToRouteResultのように「見える」必要があり、クラス階層の点で可能な限り類似している必要があります。

私はリフレクターを見てこれを行うことを考えており、RedirectToRouteResultクラスとさまざまなコントローラーの基本クラスのメソッドが行うことをすべて実行してから、拡張メソッドを介して後者をコントローラーに「追加」します。たぶん、これらは同じクラス内の静的メソッドである可能性があります。

私がこれをすることに丸くなったら、私はそれを投稿します、そうでなければ多分他の誰かが私に打ち負かすかもしれません!


0

Html.RenderActionはビューのヘルパーを利用することでこれを達成しました:

@{
    string action = ViewBag.ActionName;
    string controller = ViewBag.ControllerName;
    object routeValues = ViewBag.RouteValues;
    Html.RenderAction(action, controller, routeValues);
}

そして私のコントローラーで:

public ActionResult MyAction(....)
{
    var routeValues = HttpContext.Request.RequestContext.RouteData.Values;    
    ViewBag.ActionName = "myaction";
    ViewBag.ControllerName = "mycontroller";
    ViewBag.RouteValues = routeValues;    
    return PartialView("_AjaxRedirect");
}
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.