ASP.NET 內建 HttpContext、HttpRequest、HttpResponse 皆為具象密封類別,難以 Mock、難以擴充、綁死 Web 宿主環境。.NET 3.5 SP1 引入 System.Web.Abstractions.dll,提供 HttpContextBase、HttpRequestBase 等抽象基類與 HttpContextWrapper 適配器,徹底解耦框架與宿主,是 MVC 可測試性的基石。
HttpContextBase (abstract)
├─ HttpContextWrapper : HttpContextBase // 適配真實 HttpContext
└─ MockHttpContext : HttpContextBase // 測試用實作
HttpRequestBase → HttpRequestWrapper / MockHttpRequest
HttpResponseBase → HttpResponseWrapper / MockHttpResponse
HttpSessionStateBase → HttpSessionStateWrapper / MockHttpSession
HttpServerUtilityBase → HttpServerUtilityWrapper / MockHttpServer
| 具象類別缺點 | 抽象基類優勢 |
|---|---|
| 無法在單元測試中模擬 | 輕鬆建立 Mock/Stub 驗證邏輯 |
| 綁定 IIS/ASP.NET 管線 | 可在 Console App、Service、Azure Functions 執行 |
| 方法多為 non-virtual,無法覆寫 | 設計為可覆寫,支援擴充行為 |
靜態 HttpContext.Current 造成隱性依賴 |
透過建構子注入 HttpContextBase 明確相依 |
需求:在路由解析之前修改請求環境(如重寫路徑、注入租戶資訊)。
UrlRoutingModule.PostResolveRequestCache 為路由解析入口,首先建立 HttpContextWrapper:
private void OnApplicationPostResolveRequestCache(object sender, EventArgs e)
{
var context = new HttpContextWrapper(((HttpApplication)sender).Context);
this.PostResolveRequestCache(context); // 此處開始路由解析
}
public class ContextReplacementModule : IHttpModule
{
public void Init(HttpApplication app)
{
// 比 UrlRoutingModule 更早:PreRequestHandlerExecute
app.PreRequestHandlerExecute += OnPreRequestHandlerExecute;
}
private void OnPreRequestHandlerExecute(object sender, EventArgs e)
{
var app = (HttpApplication)sender;
var originalContext = app.Context;
// 建立自訂 HttpContextBase 實作
var customContext = new CustomHttpContext(originalContext);
// 替換內部 Context(需反射或繼承 HttpApplication)
// 此處示意:實際需透過 HttpApplication.Context 属性設定器
// 或實作自訂 HttpApplication 類別
}
}
public class CustomHttpContext : HttpContextBase
{
private readonly HttpContext _inner;
private HttpRequestBase _request;
private HttpResponseBase _response;
public CustomHttpContext(HttpContext inner) { _inner = inner; }
public override HttpRequestBase Request
{
get { return _request ?? (_request = new CustomHttpRequest(_inner.Request)); }
}
public override HttpResponseBase Response
{
get { return _response ?? (_response = new CustomHttpResponse(_inner.Response)); }
}
// 其他屬性直接委派
public override IDictionary Items => _inner.Items;
public override IPrincipal User => _inner.User;
// ...
}
public class CustomHttpRequest : HttpRequestBase
{
private readonly HttpRequest _inner;
public CustomHttpRequest(HttpRequest inner) { _inner = inner; }
public override string Path
{
get => _inner.Path;
// 可攔截修改:return ModifiedPath;
}
public override NameValueCollection QueryString => _inner.QueryString;
// ...
}
public class ProductController : Controller
{
// 悪例:直接用 HttpContext.Current
public ActionResult Bad()
{
var url = HttpContext.Current.Request.Url; // 難測試
return View();
}
// 佳例:使用基類屬性(內部已為 HttpContextBase)
public ActionResult Good()
{
var url = Request.Url; // Controller.Request 為 HttpRequestBase
var userAgent = Request.UserAgent;
return View();
}
// 最佳:建構子注入(適用於非 Controller 服務類別)
public class ProductService
{
private readonly HttpContextBase _context;
public ProductService(HttpContextBase context) { _context = context; }
public string GetClientIp() => _context.Request.UserHostAddress;
}
}
[Test]
public void ProductService_GetClientIp_ReturnsCorrectIp()
{
// Arrange
var mockContext = new Mock();
var mockRequest = new Mock();
mockRequest.Setup(r => r.UserHostAddress).Returns("192.168.1.100");
mockContext.Setup(c => c.Request).Returns(mockRequest.Object);
var service = new ProductService(mockContext.Object);
// Act
var ip = service.GetClientIp();
// Assert
Assert.AreEqual("192.168.1.100", ip);
}
HttpContext.Current 靜態存取HttpContextBase / HttpRequestBaseRequest、Response、Session 基類屬性HttpContextBaseSystem.Web.Abstractions、MoqCustomHttpRequest 解析 Host/Header 注入 TenantId 至 ItemsCustomHttpRequest 覆寫 UserLanguages 依域名/路徑決定文化特性OfflineHttpContext 讀取本地檔案/資料庫,Controller 零修改即可運行善用 System.Web.Abstractions,不僅獲得測試能力,更賦予應用程式脫離 Web 宿主、支援多種執行環境的彈性,是現代 ASP.NET 架構師的必修內功。