Como simular Server.Transfer no ASP.NET MVC?

124

No ASP.NET MVC, você pode retornar um ActionResult de redirecionamento facilmente:

 return RedirectToAction("Index");

 or

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

Isso realmente fornecerá um redirecionamento HTTP, o que normalmente é bom. No entanto, ao usar o Google Analytics, isso causa grandes problemas, pois o referenciador original está perdido, portanto o Google não sabe de onde você veio. Isso perde informações úteis, como quaisquer termos do mecanismo de pesquisa.

Como observação lateral, esse método tem a vantagem de remover qualquer parâmetro que possa ter sido proveniente de campanhas, mas ainda me permita capturá-los no servidor. Deixá-los na string de consulta leva as pessoas a marcarem favoritos, no twitter ou no blog, um link que não deveriam. Eu já vi isso várias vezes em que as pessoas enviaram links para o nosso site contendo IDs de campanha.

De qualquer forma, estou escrevendo um controlador de 'gateway' para todas as visitas recebidas no site que eu possa redirecionar para locais diferentes ou versões alternativas.

Por enquanto, eu me preocupo mais com o Google por agora (do que os marcadores acidentais) e quero poder enviar alguém que visite /a página que eles obteriam se fossem /home/7, que é a versão 7 de uma página inicial.

Como eu disse antes, se eu fizer isso, perco a capacidade do google de analisar o referenciador:

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

O que eu realmente quero é um

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

o que me proporcionará essa visualização sem um redirecionamento do lado do cliente. Eu não acho que isso exista.

Atualmente, a melhor coisa que posso criar é duplicar toda a lógica do controlador HomeController.Index(..)em minha GatewayController.Indexação. Isso significa que eu tinha que passar 'Views/Home'para 'Shared'então era acessível. Deve haver uma maneira melhor ?? ..

Simon_Weaver
fonte
O que exatamente ServerTransferActionvocê está tentando replicar? Isso é uma coisa real? (não poderia encontrar qualquer informação sobre ele ... obrigado pela pergunta, btw, a resposta abaixo é excelente)
jleach
Procure Server.Transfer (...). É uma maneira de basicamente fazer um 'redirecionamento' no lado do servidor, onde o cliente recebe a página redirecionada sem um redirecionamento do lado do cliente. Geralmente não é recomendado com roteamento moderno.
Simon_Weaver
1
"Transferir" é um recurso antigo do ASP.NET que não é mais necessário no MVC devido à capacidade de ir diretamente para a ação correta do controlador usando o roteamento. Veja esta resposta para detalhes.
precisa saber é o seguinte
@ NightOwl888 sim, definitivamente - mas também às vezes devido à lógica de negócios, é necessário / mais fácil. Olhei para trás para ver onde tinha acabado de usar isso - (felizmente, estava apenas em um lugar) - onde eu tinha uma página inicial que queria ser dinâmica para determinadas condições complexas e, nos bastidores, mostra uma rota diferente. Definitivamente, quero evitá-lo o máximo possível em favor do roteamento ou das condições da rota - mas às vezes uma ifdeclaração simples é uma solução tentadora demais.
Simon_Weaver
@ Simon_Weaver - E o que há de errado com a subclasse RouteBasepara que você possa colocar sua ifdeclaração lá em vez de dobrar tudo para trás para pular de um controlador para outro?
precisa

Respostas:

130

Que tal uma classe TransferResult? (com base na resposta de 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);
        }
    }
}

Atualizado: Agora funciona com MVC3 (usando o código da publicação de Simon ). Ele deve (não ter sido capaz de testá-lo) também trabalham em MVC2 olhando ou não ele está correndo dentro do pipeline integrado do IIS7 +.

Para total transparência; Em nosso ambiente de produção, nunca usamos o TransferResult diretamente. Usamos um TransferToRouteResult que, por sua vez, chama executa o TransferResult. Aqui está o que está realmente sendo executado nos meus servidores de produção.

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

E se você estiver usando o T4MVC (se não ... faça!), Esta extensão pode ser útil.

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

Usando esta pequena jóia você pode fazer

// in an action method
TransferToAction(MVC.Error.Index());
Markus Olsson
fonte
1
isso funciona muito bem. tenha cuidado para não terminar com um loop infinito - como fiz na primeira tentativa passando a URL errada. Fiz uma pequena modificação para permitir a passagem de uma coleção de valores de rota que pode ser útil para outras pessoas. postado acima ou abaixo ...
Simon_Weaver 7/08/09
update: esta solução parece funcionar bem e, embora eu a esteja usando apenas em uma capacidade muito limitada, ainda não encontrei nenhum problema.
Simon_Weaver 4/09/09
um problema: não é possível redirecionar da solicitação POST para GET - mas isso não é necessariamente uma coisa ruim. algo para ser cauteloso de que
Simon_Weaver
2
@ BradLaney: Você pode simplesmente remover as linhas 'var urlHelper ...' e 'var url ...' e substituir 'url' por 'this.Url' para o resto e funciona. :)
Michael Ulmann 02/02
1
1: acoplamento / teste de unidade / compatibilidade futura. 2: amostras de mvc core / mvc nunca usam esse singleton. 3: esse singleton não está disponível em um encadeamento (nulo), em um encadeamento de pool ou em um delegado assíncrono chamado em um contexto diferente do padrão, como ao usar métodos de ação assíncrona. 4: apenas para fins de compatibilidade, o mvc define esse valor singleton como context.HttpContext antes de inserir o código do usuário.
Softlion
47

Editar: atualizado para ser compatível com o ASP.NET MVC 3

Desde que você esteja usando o IIS7, a seguinte modificação parece funcionar no ASP.NET MVC 3. Graças a @nitin e @andy por apontar o código original não funcionou.

Edit 11/11/2011: TempData quebra com Server.TransferRequest a partir do MVC 3 RTM

Modificado o código abaixo para gerar uma exceção - mas nenhuma outra solução no momento.


Aqui está minha modificação baseada na versão modificada de Markus da postagem original de Stan. Eu adicionei um construtor adicional para pegar um dicionário de Valor da Rota - e renomei-o para MVCTransferResult para evitar confusão de que poderia ser apenas um redirecionamento.

Agora posso fazer o seguinte para um redirecionamento:

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

Minha classe modificada:

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);
    }
}
Simon_Weaver
fonte
1
Isso parece não estar funcionando no MVC 3 RC. Falha no HttpHandler.ProcessRequest (), diz: 'HttpContext.SetSessionStateBehavior' só pode ser chamado antes que o evento 'HttpApplication.AcquireRequestState' seja gerado.
Andy
Ainda não tive uma alteração para olhar para o MVC3. deixe-me saber se você encontrar uma solução
Simon_Weaver
O Server.TransferRquest, conforme sugerido por Nitin, faz o que o acima está tentando fazer?
amigos estão
Por que precisamos verificar TempData para null e contar> 0?
yurart
Você não, mas é apenas uma característica de segurança por isso, se você já estiver usando-lo e confiar nele, então você não vai ficar coçando sua cabeça se ele desaparece
Simon_Weaver
14

Você pode usar Server.TransferRequest no IIS7 +.

Nitin Agarwal
fonte
12

Descobri recentemente que o ASP.NET MVC não suporta Server.Transfer (), por isso criei um método stub (inspirado no 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);
    }

fonte
9

Você não poderia simplesmente criar uma instância do controlador para o qual gostaria de redirecionar, invocar o método de ação desejado e retornar o resultado disso? Algo como:

 HomeController controller = new HomeController();
 return controller.Index();
Brian Sullivan
fonte
4
Não, o controlador que você criar não terá itens como a configuração de solicitação e resposta corretamente. Isso pode levar a problemas.
Jeff Walker Code Ranger
Concordo com @JeffWalkerCodeRanger: a mesma coisa também depois de definir a propriedadeotherController.ControllerContext = this.ControllerContext;
T-MOTY
7

Eu queria redirecionar a solicitação atual para outro controlador / ação, mantendo o caminho de execução exatamente o mesmo como se o segundo controlador / ação fosse solicitado. No meu caso, Server.Request não funcionaria porque eu queria adicionar mais dados. Na verdade, é equivalente ao manipulador atual executando outro HTTP GET / POST e, em seguida, transmitindo os resultados para o cliente. Tenho certeza de que haverá maneiras melhores de conseguir isso, mas eis o que funciona para mim:

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

Seu palpite está certo: coloquei esse código em

public class RedirectOnErrorAttribute : ActionFilterAttribute, IExceptionFilter

e estou usando-o para exibir erros aos desenvolvedores, enquanto ele usa um redirecionamento regular na produção. Observe que eu não queria usar a sessão do ASP.NET, banco de dados ou outras formas de passar dados de exceção entre solicitações.


fonte
7

Em vez de simular uma transferência de servidor, o MVC ainda é capaz de realmente executar um Server.TransferRequest :

public ActionResult Whatever()
{
    string url = //...
    Request.RequestContext.HttpContext.Server.TransferRequest(url);
    return Content("success");//Doesn't actually get returned
}
AaronLS
fonte
Sinta-se à vontade para adicionar um texto à sua resposta e explicar melhor.
Wladimir Palant
Observe que isso requer MVCv3 e acima.
Seph
5

Apenas instale o outro controlador e execute seu método de ação.

Richard Szalay
fonte
Isso não mostrará o URL desejado na barra de endereços
arserbin3
@ arserbin3 - Nem Server.Transfer. Presumivelmente, esse requisito é o motivo pelo qual a pergunta original foi postada.
Richard Szalay
2

Você pode reiniciar o outro controlador e chamar o método de ação retornando o resultado. Isso exigirá que você coloque sua exibição na pasta compartilhada.

Não tenho certeza se é isso que você quis dizer com duplicado, mas:

return new HomeController().Index();

Editar

Outra opção pode ser criar seu próprio ControllerFactory, dessa forma você pode determinar qual controlador criar.

JoshBerke
fonte
essa pode ser a abordagem, mas parece não ter o contexto correto - mesmo se eu disser hc.ControllerContext = this.ControllerContext. Além disso, ele procura a visualização em ~ / Views / Gateway / 5.aspx e não a encontra.
Simon_Weaver
Além disso, você perde todos os filtros de ação. Você provavelmente deseja tentar usar o método Execute na interface IController que seus controladores devem implementar. Por exemplo: ((IController) new HomeController ()). Execute (...). Dessa forma, você ainda participa do pipeline da Action Invoker. Você teria que descobrir exatamente o que passar para Executar embora ... Refletor pode ajudar lá :)
Andrew Stanton-Nurse
Sim, eu não gosto da idéia de criar um controlador, acho melhor você definir sua própria fábrica de controladores, que parece ser o ponto de extensão apropriado para isso. Mas eu apenas arranhei a superfície dessa estrutura para estar longe.
JoshBerke
1

O roteamento não cuida desse cenário para você? ou seja, para o cenário descrito acima, você pode apenas criar um manipulador de rota que implemente essa lógica.

Richard
fonte
é baseado em condições programáticas. ie campanha 100 pode ir para ver 7 e campanha 200 pode ir para ver 8 etc. etc. muito complicado para roteamento
Simon_Weaver
4
Por que isso é muito complicado para roteamento? O que há de errado com as restrições de rota personalizadas? stephenwalther.com/blog/archive/2008/08/07/…
Ian Mercer
1

Para quem usa roteamento baseado em expressão, usando apenas a classe TransferResult acima, aqui está um método de extensão de controlador que executa o truque e preserva TempData. Não há necessidade de 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);
}
Stephane Legay
fonte
Aviso: isso parece causar um erro 'A classe SessionStateTempDataProvider requer que o estado da sessão seja ativado', embora ainda funcione. Eu só vejo esse erro nos meus logs. Estou usando ELMAH para o registo de erro e obter este erro para InProc e AppFabric
Simon_Weaver
1

Server.TransferRequesté completamente desnecessário no MVC . Esse é um recurso antiquado que só era necessário no ASP.NET porque a solicitação veio diretamente para uma página e precisava haver uma maneira de transferir uma solicitação para outra página. As versões modernas do ASP.NET (incluindo MVC) têm uma infraestrutura de roteamento que pode ser personalizada para rotear diretamente para o recurso desejado. Não há sentido em deixar a solicitação chegar a um controlador apenas para transferi-la para outro controlador quando você pode simplesmente fazer com que a solicitação vá diretamente para o controlador e a ação que você deseja.

Além disso, como você está respondendo à solicitação original , não há necessidade de colocar nada TempDataou outro armazenamento apenas para encaminhar a solicitação para o local certo. Em vez disso, você chega à ação do controlador com a solicitação original intacta. Você também pode ter certeza de que o Google aprovará essa abordagem, pois ela acontece inteiramente no lado do servidor.

Embora você possa fazer bastante com ambos IRouteConstrainte IRouteHandler, o ponto de extensão mais poderoso para roteamento é a RouteBasesubclasse. Essa classe pode ser estendida para fornecer rotas de entrada e geração de URL de saída, o que o torna um balcão único para tudo o que tem a ver com o URL e a ação que o URL executa.

Portanto, para seguir seu segundo exemplo, para ir de /para /home/7, basta uma rota que adicione os valores de rota apropriados.

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

Mas voltando ao seu exemplo original, em que você tem uma página aleatória, é mais complexo porque os parâmetros da rota não podem ser alterados em tempo de execução. Portanto, isso pode ser feito com uma RouteBasesubclasse da seguinte maneira.

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

Que pode ser registrado no roteamento como:

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

Observe no exemplo acima, pode fazer sentido também armazenar um cookie registrando a versão da página inicial em que o usuário entrou, para que, quando retornem, recebam a mesma versão da página inicial.

Observe também que, usando essa abordagem, você pode personalizar o roteamento para levar em consideração os parâmetros da string de consulta (por padrão os ignora completamente) e encaminhar para uma ação apropriada do controlador.

Exemplos adicionais

NightOwl888
fonte
E se eu não quiser transferir imediatamente ao entrar em uma ação, mas permitir que essa ação faça algum trabalho e depois transfira condicionalmente para outra ação. Alterar meu roteamento para ir diretamente para o destino da transferência não funcionará, portanto parece que Server.TransferRequestnão é "completamente desnecessário no MVC".
ProfK 29/01
0

Não é uma resposta em si, mas claramente o requisito seria não apenas para a navegação real "executar" a funcionalidade equivalente do Webforms Server.Transfer (), mas também para que tudo isso seja totalmente suportado nos testes de unidade.

Portanto, o ServerTransferResult deve "parecer" um RedirectToRouteResult e ser o mais semelhante possível em termos da hierarquia de classes.

Estou pensando em fazer isso olhando para Reflector e fazendo o que quer que seja a classe RedirectToRouteResult e também os vários métodos da classe base do Controller, e depois "adicionando" a última ao Controller por meio de métodos de extensão. Talvez estes possam ser métodos estáticos dentro da mesma classe, para facilitar / preguiça de baixar?

Se eu começar a fazer isso, publicarei, caso contrário, talvez alguém possa me derrotar!

William
fonte
0

Consegui isso aproveitando o Html.RenderActionajudante em uma View:

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

E no meu controlador:

public ActionResult MyAction(....)
{
    var routeValues = HttpContext.Request.RequestContext.RouteData.Values;    
    ViewBag.ActionName = "myaction";
    ViewBag.ControllerName = "mycontroller";
    ViewBag.RouteValues = routeValues;    
    return PartialView("_AjaxRedirect");
}
Colin
fonte