【新瓶裝老酒】將 Response.Write/Redirect 邏輯移植到 ASP.NET Core MVC Action
0 |
在 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