Bagaimana untuk mensimulasikan Server.Transfer di ASP.NET MVC?


124

Di ASP.NET MVC, Anda dapat mengembalikan ActionResult dengan cukup mudah:

 return RedirectToAction("Index");

 or

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

Ini sebenarnya akan memberikan pengalihan HTTP, yang biasanya baik-baik saja. Namun, ketika menggunakan google analytics, hal ini menyebabkan masalah besar karena perujuk asli hilang sehingga google tidak tahu dari mana Anda berasal. Ini akan menghilangkan informasi berguna seperti istilah mesin pencari.

Sebagai catatan tambahan, metode ini memiliki keuntungan untuk menghapus semua parameter yang mungkin berasal dari kampanye tetapi masih memungkinkan saya untuk menangkapnya dari sisi server. Membiarkannya dalam string kueri mengarahkan orang ke bookmark atau twitter atau blog link yang seharusnya tidak mereka lakukan. Saya telah melihat ini beberapa kali ketika orang-orang menge-tweet tautan ke situs kami yang berisi ID kampanye.

Bagaimanapun, saya menulis pengontrol 'gerbang' untuk semua kunjungan masuk ke situs yang dapat saya arahkan ke tempat yang berbeda atau versi alternatif.

Untuk saat ini saya lebih peduli tentang Google untuk saat ini (daripada bookmark yang tidak disengaja), dan saya ingin dapat mengirim seseorang yang mengunjungi /ke halaman yang akan mereka dapatkan jika mereka membukanya /home/7, yang merupakan versi 7 dari sebuah beranda.

Seperti yang saya katakan sebelumnya Jika saya melakukan ini, saya kehilangan kemampuan google untuk menganalisis perujuk:

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

Yang saya inginkan adalah a

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

yang akan memberi saya tampilan tersebut tanpa pengalihan sisi klien. Saya tidak berpikir hal seperti itu ada.

Saat ini hal terbaik yang dapat saya hasilkan adalah menduplikasi seluruh logika pengontrol HomeController.Index(..)dalam GatewayController.IndexTindakan saya . Ini berarti saya harus pindah 'Views/Home'ke 'Shared'jadi itu dapat diakses. Pasti ada cara yang lebih baik ?? ..


Apa sebenarnya ServerTransferActionyang coba Anda tiru? Apakah itu hal yang nyata? (tidak dapat menemukan info apa pun tentang itu ... terima kasih atas pertanyaannya, btw, jawaban di bawah ini luar biasa)
jleach

Cari Server.Transfer (...). Ini pada dasarnya adalah cara untuk melakukan 'pengalihan' di sisi server tempat klien menerima laman yang dialihkan tanpa pengalihan sisi klien. Umumnya tidak disarankan dengan perutean modern.
Simon_Weaver

1
"Mentransfer" adalah fitur ASP.NET kuno yang tidak lagi diperlukan di MVC karena kemampuan untuk langsung ke tindakan pengontrol yang benar menggunakan perutean. Lihat jawaban ini untuk detailnya.
NightOwl888

@ NightOwl888 ya pasti - tetapi kadang-kadang juga karena logika bisnis itu perlu / lebih mudah. Saya melihat ke belakang untuk melihat di mana saya akhirnya menggunakan ini - (untungnya itu hanya di satu tempat) - di mana saya memiliki beranda yang saya ingin dinamis untuk kondisi kompleks tertentu dan di belakang layar itu menunjukkan rute yang berbeda. Pasti ingin menghindarinya sebanyak mungkin demi kondisi rute atau rute - tetapi terkadang ifpernyataan sederhana terlalu menggoda solusi.
Simon_Weaver

@Simon_Weaver - Dan apa yang salah dengan subclassing RouteBasesehingga Anda dapat meletakkan ifpernyataan Anda di sana alih-alih membengkokkan semuanya ke belakang untuk melompat dari satu pengontrol ke pengontrol lainnya?
NightOwl888

Jawaban:


130

Bagaimana dengan kelas TransferResult? (berdasarkan jawaban Stans )

/// <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);
        }
    }
}

Diperbarui: Sekarang bekerja dengan MVC3 (menggunakan kode dari posting Simon ). Ini seharusnya (belum mampu untuk menguji itu) juga bekerja di MVC2 dengan melihat apakah atau tidak itu berjalan dalam pipa terpadu IIS7 +.

Untuk transparansi penuh; Dalam lingkungan produksi kami, kami tidak pernah menggunakan TransferResult secara langsung. Kami menggunakan TransferToRouteResult yang pada gilirannya panggilan mengeksekusi TransferResult tersebut. Inilah yang sebenarnya berjalan di server produksi saya.

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);
    }
}

Dan jika Anda menggunakan T4MVC (jika tidak ... lakukan!) Ekstensi ini mungkin berguna.

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

Menggunakan permata kecil ini bisa Anda lakukan

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

1
ini bekerja dengan baik. berhati-hatilah agar tidak berakhir dengan loop tak terbatas - seperti yang saya lakukan pada upaya pertama saya dengan memasukkan URL yang salah. Saya membuat sedikit modifikasi untuk memungkinkan kumpulan nilai rute diteruskan yang mungkin berguna bagi orang lain. diposting di atas atau di bawah ...
Simon_Weaver

memperbarui: solusi ini tampaknya berfungsi dengan baik, dan meskipun saya menggunakannya hanya dalam kapasitas yang sangat terbatas belum menemukan masalah apa pun
Simon_Weaver

satu masalah: tidak dapat mengalihkan dari permintaan POST ke GET - tetapi itu belum tentu merupakan hal yang buruk. sesuatu yang harus diwaspadai
Simon_Weaver

2
@BradLaney: Anda dapat menghapus baris 'var urlHelper ...' dan 'var url ...' dan mengganti 'url' dengan 'this.Url' untuk selebihnya dan berfungsi. :)
Michael Ulmann

1
1: pengujian kopling / unit / kompatibilitas di masa depan. 2: sampel inti mvc / mvc tidak pernah menggunakan tunggal ini. 3: tunggal ini tidak tersedia dalam utas (null), baik utas kumpulan atau delegasi asinkron yang dipanggil pada konteks selain default, seperti saat menggunakan metode tindakan asinkron. 4: untuk tujuan kompatibilitas saja, mvc menyetel nilai tunggal ini ke context.HttpContext sebelum memasukkan kode pengguna.
Softlion

47

Sunting: Diperbarui agar kompatibel dengan ASP.NET MVC 3

Asalkan Anda menggunakan IIS7, modifikasi berikut tampaknya berfungsi untuk ASP.NET MVC 3. Terima kasih kepada @nitin dan @andy karena menunjukkan kode asli tidak berfungsi.

Sunting 4/11/2011: TempData putus dengan Server.TransferRequest pada MVC 3 RTM

Memodifikasi kode di bawah ini untuk membuat pengecualian - tetapi tidak ada solusi lain untuk saat ini.


Berikut modifikasi saya berdasarkan versi modifikasi Markus dari posting asli Stan. Saya menambahkan konstruktor tambahan untuk mengambil kamus Route Value - dan menamainya MVCTransferResult untuk menghindari kebingungan bahwa itu mungkin hanya pengalihan.

Sekarang saya dapat melakukan hal berikut untuk pengalihan:

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

Kelas saya yang dimodifikasi:

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
Ini sepertinya tidak berfungsi di MVC 3 RC. Gagal di HttpHandler.ProcessRequest (), mengatakan: 'HttpContext.SetSessionStateBehavior' hanya bisa dipanggil sebelum acara 'HttpApplication.AcquireRequestState' dimunculkan.
Andy

saya belum punya perubahan untuk melihat MVC3. beri tahu saya jika Anda menemukan solusi
Simon_Weaver

Apakah Server.TransferRquest seperti yang disarankan oleh Nitin melakukan apa yang coba dilakukan di atas?
Old Geezer

Mengapa kita perlu memeriksa TempData untuk null dan count> 0?
yurart

Tidak, tapi ini hanya fitur keamanan jadi jika Anda sudah menggunakannya dan mengandalkannya maka Anda tidak akan dibiarkan menggaruk-garuk kepala jika menghilang
Simon_Weaver


12

Saya baru-baru ini mengetahui bahwa ASP.NET MVC tidak mendukung Server.Transfer () jadi saya telah membuat metode rintisan (terinspirasi oleh 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

Tidak bisakah Anda membuat instance dari pengontrol yang ingin Anda alihkan, menjalankan metode tindakan yang Anda inginkan, lalu mengembalikan hasilnya? Sesuatu seperti:

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

4
Tidak, pengontrol yang Anda buat tidak akan memiliki hal-hal seperti Permintaan dan Respons yang disiapkan dengan benar. Itu bisa menimbulkan masalah.
Jeff Walker Code Ranger

Saya setuju dengan @JeffWalkerCodeRanger: hal yang sama juga setelah mengatur propertiotherController.ControllerContext = this.ControllerContext;
T-moty

7

Saya ingin merutekan ulang permintaan saat ini ke pengontrol / tindakan lain, sambil menjaga jalur eksekusi persis sama seperti jika pengontrol / tindakan kedua diminta. Dalam kasus saya, Server.Request tidak akan berfungsi karena saya ingin menambahkan lebih banyak data. Ini sebenarnya setara dengan penangan saat ini yang menjalankan HTTP GET / POST lain, lalu mengalirkan hasilnya ke klien. Saya yakin akan ada cara yang lebih baik untuk mencapai ini, tetapi inilah yang berhasil untuk saya:

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);

Tebakan Anda benar: Saya memasukkan kode ini

public class RedirectOnErrorAttribute : ActionFilterAttribute, IExceptionFilter

dan saya menggunakannya untuk menampilkan kesalahan kepada pengembang, sementara itu akan menggunakan pengalihan biasa dalam produksi. Perhatikan bahwa saya tidak ingin menggunakan sesi ASP.NET, database, atau beberapa cara lain untuk melewatkan data pengecualian antara permintaan.


7

Daripada mensimulasikan sebuah server transfer, MVC masih mampu benar-benar melakukan Server.TransferRequest :

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

Jangan ragu untuk menambahkan beberapa teks ke jawaban Anda untuk menjelaskannya lebih lanjut.
Wladimir Palant

Perhatikan bahwa ini membutuhkan MVCv3 ke atas.
Seph

5

Cukup contoh pengontrol lain dan jalankan metode aksinya.


Ini tidak akan menampilkan URL yang diinginkan di bilah alamat
arserbin3

@ arserbin3 - Begitu pula Server.Transfer. Persyaratan ini mungkin menjadi alasan mengapa pertanyaan asli bahkan diposting.
Richard Szalay

2

Anda dapat memperbarui pengontrol lain dan menjalankan metode tindakan yang mengembalikan hasilnya. Ini akan meminta Anda untuk menempatkan tampilan Anda ke folder bersama.

Saya tidak yakin apakah ini yang Anda maksud dengan duplikat, tetapi:

return new HomeController().Index();

Sunting

Pilihan lainnya adalah membuat ControllerFactory Anda sendiri, dengan cara ini Anda dapat menentukan pengontrol mana yang akan dibuat.


ini mungkin pendekatannya, tetapi tampaknya tidak cukup memiliki konteks yang benar - bahkan jika saya katakan hc.ControllerContext = this.ControllerContext. Ditambah itu kemudian mencari tampilan di bawah ~ / Views / Gateway / 5.aspx dan tidak menemukannya.
Simon_Weaver

Ditambah Anda kehilangan semua Filter Tindakan. Anda mungkin ingin mencoba menggunakan metode Execute pada antarmuka IController yang harus diterapkan oleh pengontrol Anda. Misalnya: ((IController) new HomeController ()). Execute (...). Dengan begitu Anda masih berpartisipasi dalam pipeline Action Invoker. Anda harus mencari tahu dengan tepat apa yang harus diteruskan ke Execute ... Reflector mungkin membantu di sana :)
Andrew Stanton-Nurse

Ya, saya tidak suka gagasan untuk membuat pengontrol baru, saya pikir lebih baik Anda menentukan pabrik pengontrol Anda sendiri yang sepertinya merupakan titik ekstensi yang tepat untuk ini. Tapi saya baru saja menggores permukaan kerangka ini jadi saya mungkin salah.
JoshBerke

1

Bukankah perutean hanya menangani skenario ini untuk Anda? yaitu untuk skenario yang dijelaskan di atas, Anda bisa membuat penangan rute yang menerapkan logika ini.


itu didasarkan pada kondisi programatik. misalnya, kampanye 100 mungkin pergi ke tampilan 7 dan kampanye 200 mungkin pergi ke tampilan 8 dll. terlalu rumit untuk perutean
Simon_Weaver

4
Mengapa itu terlalu rumit untuk perutean? Apa yang salah dengan batasan rute kustom? stephenwalther.com/blog/archive/2008/08/07/…
Ian Mercer

1

Untuk siapa pun yang menggunakan perutean berbasis ekspresi, hanya menggunakan kelas TransferResult di atas, berikut adalah metode ekstensi pengontrol yang melakukan trik dan mempertahankan TempData. Tidak perlu 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);
}

Peringatan: ini tampaknya menyebabkan kesalahan 'Kelas SessionStateTempDataProvider memerlukan status sesi untuk diaktifkan' meskipun sebenarnya masih berfungsi. Saya hanya melihat kesalahan ini di log saya. Saya menggunakan ELMAH untuk pencatatan kesalahan dan mendapatkan kesalahan ini untuk InProc dan AppFabric
Simon_Weaver

1

Server.TransferRequestsama sekali tidak diperlukan di MVC . Ini adalah fitur kuno yang hanya diperlukan di ASP.NET karena permintaan datang langsung ke halaman dan perlu ada cara untuk mentransfer permintaan ke halaman lain. Versi modern ASP.NET (termasuk MVC) memiliki infrastruktur perutean yang dapat disesuaikan untuk merutekan langsung ke sumber daya yang diinginkan. Tidak ada gunanya membiarkan permintaan mencapai pengontrol hanya untuk mentransfernya ke pengontrol lain saat Anda dapat membuat permintaan langsung ke pengontrol dan tindakan yang Anda inginkan.

Terlebih lagi, karena Anda menanggapi permintaan asli , tidak perlu memasukkan apa pun ke dalam TempDataatau penyimpanan lain hanya untuk mengarahkan permintaan ke tempat yang tepat. Sebagai gantinya, Anda sampai pada aksi pengontrol dengan permintaan asli utuh. Anda juga dapat yakin bahwa Google akan menyetujui pendekatan ini karena ini terjadi sepenuhnya di sisi server.

Meskipun Anda dapat melakukan sedikit dari keduanya IRouteConstraintdan IRouteHandler, titik ekstensi paling kuat untuk perutean adalah RouteBasesubkelas. Kelas ini dapat diperluas untuk menyediakan rute masuk dan pembuatan URL keluar, yang menjadikannya tempat serba ada untuk segala sesuatu yang berkaitan dengan URL dan tindakan yang dijalankan URL.

Jadi, untuk mengikuti contoh kedua Anda, untuk pergi dari /ke /home/7, Anda hanya memerlukan rute yang menambahkan nilai rute yang sesuai.

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 }
        );
    }
}

Tetapi kembali ke contoh asli Anda di mana Anda memiliki halaman acak, itu lebih rumit karena parameter rute tidak dapat berubah saat runtime. Jadi, bisa dilakukan dengan RouteBasesubclass sebagai berikut.

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;
    }
}

Yang dapat didaftarkan dalam perutean seperti:

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 }
        );
    }
}

Perhatikan dalam contoh di atas, mungkin masuk akal untuk juga menyimpan cookie yang merekam versi halaman beranda tempat pengguna masuk sehingga ketika kembali, mereka menerima versi halaman beranda yang sama.

Perhatikan juga bahwa menggunakan pendekatan ini Anda dapat menyesuaikan perutean untuk mempertimbangkan parameter string kueri (ini sepenuhnya mengabaikannya secara default) dan merutekan ke tindakan pengontrol yang sesuai.

Contoh Tambahan


Bagaimana jika saya tidak ingin langsung mentransfer saat memasukkan tindakan, melainkan membiarkan tindakan tersebut melakukan beberapa pekerjaan dan kemudian secara kondisional mentransfer ke tindakan lain. Mengubah perutean saya untuk langsung ke target transfer tidak akan berfungsi, jadi sepertinya Server.TransferRequestbukan "sama sekali tidak perlu di MVC".
ProfK

0

Bukan jawaban itu sendiri, tetapi jelas persyaratannya tidak hanya untuk navigasi yang sebenarnya untuk "melakukan" fungsi yang setara dari Webforms Server.Transfer (), tetapi juga agar semua ini didukung sepenuhnya dalam pengujian unit.

Oleh karena itu, ServerTransferResult harus "terlihat" seperti RedirectToRouteResult, dan semirip mungkin dalam hal hierarki kelas.

Saya berpikir untuk melakukan ini dengan melihat Reflector, dan melakukan apa pun kelas RedirectToRouteResult dan juga berbagai metode kelas dasar Controller lakukan, dan kemudian "menambahkan" yang terakhir ke Controller melalui metode ekstensi. Mungkin ini bisa menjadi metode statis dalam kelas yang sama, untuk kemudahan / kemalasan mengunduh?

Jika saya sempat melakukan ini, saya akan mempostingnya, jika tidak, mungkin orang lain akan mengalahkan saya!


0

Saya mencapai ini dengan memanfaatkan Html.RenderActionhelper dalam sebuah View:

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

Dan di pengontrol saya:

public ActionResult MyAction(....)
{
    var routeValues = HttpContext.Request.RequestContext.RouteData.Values;    
    ViewBag.ActionName = "myaction";
    ViewBag.ControllerName = "mycontroller";
    ViewBag.RouteValues = routeValues;    
    return PartialView("_AjaxRedirect");
}
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.