在 ASP.NET WebForm 時代,Reponse.Write()/Response.Redirect() 是從共用元件操縱網頁流程蠻常用的技巧,醜歸醜,在一些場合能用少少程式碼解決問題,亦不失為一個選項。

舉個例子,假設有段網頁共用邏輯被寫在 App_Code/LegacyLib.cs - 依 ?fw=blog 參數導向預設網站、缺少 id 參數時顯示提示訊息並中止網頁:

using System.Web;

public class LegacyLib
{
    public static void Process(HttpRequest request, HttpResponse response)
    {
        if (request["fw"] == "blog")
        {
            response.Redirect("https://blog.darkthread.net");
            response.End();
        }
        else if (string.IsNullOrEmpty(request["id"]))
        {
            response.Write("Missing id parameter");
            response.End();
        }
    }
}

要套用這段邏輯的 WebForm 網頁可以這樣寫:

<%@Page Language="C#" %>
<script runat="server">
    void Page_Load(object sender, EventArgs e)
    {
        LegacyLib.Process(Request, Response);
        Response.ContentType = "text/plain";
        Response.Write("OK");
    }
</script>

如此,未給參數時會顯示 Missing id parameter,加上 ?id=XX 參數顯示 OK,改為 ?fw=blog 則導向其他網站:

這個邏輯是沒法直接搬進 ASP.NET Core 的。

在 ASP.NET Core,Response 的型別是 Microsoft.AspNetCore.Http.HttpRequest,與 .NET Framework System.Web.HttpRequest 是兩個不同類別,不再提供 Write()/End() 等上古方法。

此時的正統開發策略應是用 ASP.NET Core 思維重新設計共用元件,善用 IActionFilter、Middleware 等機制,充分發揮新框架優點。

BUT! 你懂的,有時全村的希望是要你小改舊元件,讓它相容新框架儘快上線,沒人叫你砍掉重練~

這篇就來探討在 ASP.NET Core MVC 套用 Response.Write()/Redirect() 幾種可行的做法。

用一個 ASP.NET Core Minimal API 專案啟用 MVC 來模擬應用情境:

using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);

// 啟用 Controller
builder.Services.AddControllers();

var app = builder.Build();

// 指定 Controller 路由
app.MapControllers();
app.MapControllerRoute(name: "default", pattern: "{controller=Home}/{action=Index}/{id?}");

app.Run();
 
public class HomeController : Controller  
{
    public IActionResult Index() 
    {
        return Content("Hello, World!");
    }
    public IActionResult RespWrite() 
    {
        // TODO: 在這裡呼叫 LegacyLib.Process(Request, Response));
        return Content("OK");
    }
}

來看看元件要怎麼改。Microsoft.AspNetCore.Http.HttpRequest 雖然沒有 Write()/End(),但有 WriteAsync(),而 Redirect() 也還在,至於沒有 End() 的問題,則讓 Process() 回傳 bool 判別是否應終止流程。(註:另個粗暴做法是自幹 Thread.CurrentThread.Abort(),我不愛)

基於以上概念隨意改寫如下:

public class LegacyLib {
    public static bool Process(HttpRequest request, HttpResponse response) 
    {
        if (request.Query["fw"] == "blog") 
        {
            response.Redirect("https://blog.darkthread.net");
            return true;
        }
        else if (string.IsNullOrEmpty(request.Query["id"])) 
        {
            response.WriteAsync("Missing id parameter").Wait();
            return true;
        }
        return false;
    }
}

呼叫端則可改寫成以下形式:

public class HomeController : Controller  
{
    public IActionResult Index() 
    {
        return Content("Hello, World!");
    }
    public IActionResult RespWrite() 
    {
        if (LegacyLib.Process(Request, Response))
        {
            return Content(string.Empty);
        }
        return Content("OK");
    }
}

經過這番小調整,舊元件「幾乎」就可以在 ASP.NET Core MVC 正常運作了。

所謂「幾乎」是這個做法在 IISExpress 或 IIS 可順利運行題,但跑在 Kestrel 則只有 ?fw=blog / Redirect() OK,未給參數時 Response.Write() 的訊息不會顯示,且瀏覽器會出現 net::ERR_INCOMPLETE_CHUNKED_ENCODING 錯誤:

伺服器端則顯示 System.InvalidOperationException: Headers are read-only, response has already started.,判斷是 Kestrel 底層處理不相容 Response.WriteAsync() 預先寫入內容的做法。

因此,如果網站要部署在 IIS,也只會在 Visual Studio 用 IISExpress 偵錯,用以上的簡便改法就算過關。

若 ASP.NET Core 網站要部署到 Docker,還想在 VSCode 正常執行跟偵測怎麼辦?

解法一,MVC 不要回傳 IActionResult,改傳 Task;要回傳內容也改用 Response.WriteAsync() 輸出,但簡單輸出還行,若原本是要顯示 View 就棘手了。

public async Task RespWrite()
{
    if (LegacyLib.Process(Request, Response))
    {
        return;
    }
    await Response.WriteAsync("OK");
}

若做到與 MVC 高度相容,以下是我想到的做法。為 ASP.NET Core HttpResponse 加上擴充方法 .LegacyWrite() 及 .LegacyRedirect() 模擬 .Write()/.Redirect(),其原理是將輸出資料存入 Headers,呼叫端透過 Response.GetLegacyResponseResult() 再轉為 RedirectResult 或 ContentResult 當成 Action 回傳需要的 IActionReult。

public class HomeController : Controller
{
    public IActionResult Index()
    {
        return Content("Hello, World!");
    }
    public IActionResult RespWrite()
    {
        if (LegacyLib.Process(Request, Response))
        {
            return Response.GetLegacyResponseResult();
        }
        return Content("OK");
    }
}

public class LegacyLib
{
    public static bool Process(HttpRequest request, HttpResponse response)
    {
        if (request.Query["fw"] == "blog")
        {
            response.LegacyRedirect("https://blog.darkthread.net");
            return true;
        }
        else if (string.IsNullOrEmpty(request.Query["id"]))
        {
            response.LegacyWrite("Missing id parameter");
            return true;
        }
        return false;
    }
}

public static class RespWriteExtensions
{
    public static void LegacyWrite(this HttpResponse response, string content)
        => response.Headers.Append("X-Response-Write", Uri.EscapeDataString(content));
    public static void LegacyRedirect(this HttpResponse response, string url)
        => response.Headers.Append("X-Response-Redirect", Uri.EscapeDataString(url));
    public static IActionResult GetLegacyResponseResult(this HttpResponse response)
    {
        var data = new Dictionary<string, string>();
        response.Headers.Where(h => h.Key.StartsWith("X-Response-")).ToList().ForEach(h =>
        {
            if (data.ContainsKey(h.Key))
                data[h.Key] += Uri.UnescapeDataString(h.Value.ToString());
            else
                data[h.Key] = Uri.UnescapeDataString(h.Value.ToString());
            response.Headers.Remove(h.Key);
        });
        if (data.TryGetValue("X-Response-Redirect", out var url))
            return new RedirectResult(url);
        if (data.TryGetValue("X-Response-Write", out var content))
            return new ContentResult
            {
                Content = content,
                ContentType = response.ContentType ?? "text/html"
            };
        throw new InvalidOperationException("No legacy response found");
    }
}

如此,共用元件只需將 WriteAsync()、Redirect() 改成 LegacyWrite() 及 LegacyRedirect(),MVC Action 方法 return Response.GetLegacyResponseResult(); 就行囉! 打完收工。

The article explores strategies for adapting legacy ASP.NET WebForm techniques like Response.Write() and Response.Redirect() in ASP.NET Core MVC. It demonstrates a minimal code modification approach and proposes a robust solution using extension methods to maintain compatibility and functionality across different hosting environments.


Comments

Be the first to post a comment

Post a comment