2016 年六月

第 31 卷,第 6 期

本文章是由機器翻譯。

ASP.NET - 使用自訂中介軟體偵測與修正 ASP.NET Core 應用程式中的 404

Steve Smith

如果您曾經掉項目在學校或純娛樂 park,您可能已有良好的 fortune 取得上一步是藉由檢查失物招領的位置。在 Web 應用程式,使用者經常建立路徑不是由伺服器處理的要求產生 404 找不到回應碼 (和偶爾幽默的頁面,向使用者解釋問題)。一般而言,是由使用者尋找他們所尋找的目標各自獨立的透過重複的猜測或可能使用搜尋引擎。不過中, 介軟體的位元,您可以加入 「 失物招領 」 至 ASP.NET 核心應用程式可協助使用者尋找他們所需要的資源。

什麼是中介軟體?

ASP.NET 核心文件會定義為 「 會組裝至應用程式管線,來處理要求和回應的元件。 」 的中介軟體 簡單來說中, 介軟體會要求委派,它可以表示做為 lambda 運算式,像這樣 ︰

app.Run(async context => {
  await context.Response.WriteAsync(“Hello world”);
});

如果您的應用程式只是中介軟體這一個位元所組成,則會傳回"Hello world"為每個要求。因為它未參考下一步的中介軟體,在此範例中,即終止管線 — 執行定義之後就會執行任何動作。不過,因為它是在管線一端並不表示您不能 「 包含 」 在其他中介軟體。比方說,您可以加入一些中介軟體,將標頭加入至先前的回應 ︰

app.Use(async (context, next) =>
{
  context.Response.Headers.Add("Author", "Steve Smith");
  await next.Invoke();
});
app.Run(async context =>
{
  await context.Response.WriteAsync("Hello world ");
});

應用程式呼叫。使用包裝的應用程式的呼叫。執行時,呼叫它使用下一步]。叫用。當您撰寫您自己的中介軟體時,您可以在管線中選擇您是否想要它之後,執行作業之前,或之前和之後的下一個中介軟體。您也可以選擇不接下來呼叫,以縮短管線。我將示範如何,這可協助您建立為 404 修正中介軟體。

如果您使用預設的核心 MVC 範本,您將不會在初始啟動檔案中找到這類低階委派為基礎的中介軟體程式碼。建議您將在它自己的類別中中, 介軟體封裝,並提供資料可以從啟動呼叫擴充方法 (名為 UseMiddlewareName)。內建的 ASP.NET 中介軟體遵循此慣例,這些呼叫所示範 ︰

if (env.IsDevelopment())
{
  app.UseDeveloperExceptionPage();
}
app.UseStaticFiles()
app.UseMvc();

您的中介軟體的順序很重要。在上述程式碼,UseDeveloperExceptionPage (這只設定在應用程式是在開發環境中執行) 的呼叫應該會換行 (因此加入之前) 任何其他中介軟體,可能會產生錯誤。

在它自己的類別中

我不想雜亂無章的 lambda 運算式和詳細的實作,我的中介軟體的所有我啟動類別。就像使用內建的中介軟體,我想我要加入至管線,其中含有另一行程式碼的中介軟體。我也預期我的中介軟體將會需要插入使用相依性插入 (DI) 之後的中介軟體已重構為自己的類別,輕鬆實現的服務。(請參閱我月文章,網址 msdn.com/magazine/mt703433 如需 ASP.NET core DI。)

因為我使用 Visual Studio,我可以使用 [加入新項目,然後選擇 [中介軟體類別] 範本加入中介軟體。[圖 1 顯示這個範本會產生,包括加入 UseMiddleware 透過管線的中介軟體的擴充方法的預設內容。

[圖 1 中介軟體類別樣板

public class MyMiddleware
{
  private readonly RequestDelegate _next;
  public MyMiddleware(RequestDelegate next)
  {
    _next = next;
  }
  public Task Invoke(HttpContext httpContext)
  {
    return _next(httpContext);
  }
}
// Extension method used to add the middleware to the HTTP request pipeline.
public static class MyMiddlewareExtensions
{
  public static IApplicationBuilder UseMyMiddleware(this IApplicationBuilder builder)
  {
    return builder.UseMiddleware<MyMiddleware>();
  }
}

一般而言,我加入非同步叫用方法簽章,然後變更其主體 ︰

await _next(httpContext);

這使得非同步引動過程。

當我建立了不同的中介軟體類別時,我進入我委派邏輯叫用方法。然後我會使用 UseMyMiddleware 擴充方法的呼叫取代中設定的呼叫。在此時執行應用程式應該確認,就像一樣,設定類別就能輕鬆組成一系列 UseSomeMiddleware 陳述式時要遵循的中介軟體仍會。

找不到偵測和錄製 404 回應

在 ASP.NET 應用程式提出要求時,如果不符合任何處理常式,回應會包含設定為 404 的 StatusCode。我可以建立一些中介軟體,會檢查此回應碼 (在之後呼叫 _next),並採取某些動作記錄要求的詳細資料 ︰

await _next(httpContext);
if (httpContext.Response.StatusCode == 404)
{
  _requestTracker.Record(httpContext.Request.Path);
}

我想要能夠追蹤的多少 404 已有特定的路徑,因此我可以修正最常見的並充分利用我修正動作。若要這樣做,請建立稱為 RequestTracker,其路徑為基礎的記錄執行個體的 404 的要求的服務。RequestTracker 都會透過傳遞至我的中介軟體 DI,如所示 [圖 2

[圖 2 將 RequestTracker 傳遞到中介軟體的相依性插入

public class NotFoundMiddleware
{
  private readonly RequestDelegate _next;
  private readonly RequestTracker _requestTracker;
  private readonly ILogger _logger;
  public NotFoundMiddleware(RequestDelegate next,
    ILoggerFactory loggerFactory,
    RequestTracker requestTracker)
  {
    _next = next;
    _requestTracker = requestTracker;
    _logger = loggerFactory.CreateLogger<NotFoundMiddleware>();
  }
}

若要將 NotFoundMiddleware 加入至我的管線,我會呼叫 UseNotFoundMiddleware 擴充方法。不過,因為它現在會相依於正在設定服務容器中的自訂服務,我還需要確保登錄此服務。我呼叫 AddNotFoundMiddleware IServiceCollection 上建立的擴充方法,並在 ConfigureServices 啟始呼叫這個方法 ︰

public static IServiceCollection AddNotFoundMiddleware(
  this IServiceCollection services)
{
  services.AddSingleton<INotFoundRequestRepository,
    InMemoryNotFoundRequestRepository>();
  return services.AddSingleton<RequestTracker>();
}

在此情況下,我 AddNotFoundMiddleware 方法可確保讓它可以在建立時,插入 NotFoundMiddleware 我 RequestTracker 的執行個體設定為 [服務] 容器中的單一。它也會向上 INotFoundRequestRepository RequestTracker 用來將其資料保存在記憶體中實作。

因為許多同時要求相同遺漏路徑中的程式碼,可能會 [圖 3 以確保沒有加入重複的執行個體的 NotFoundRequest,且計數都會增加正確使用簡單的鎖定。

[圖 3 RequestTracker

public class RequestTracker
{
  private readonly INotFoundRequestRepository _repo;
  private static object _lock = new object();
  public RequestTracker(INotFoundRequestRepository repo)
  {
    _repo = repo;
  }
  public void Record(string path)
  {
    lock(_lock)
    {
      var request = _repo.GetByPath(path);
      if (request != null)
      {
        request.IncrementCount();
      }
      else
      {
        request = new NotFoundRequest(path);
        request.IncrementCount();
        _repo.Add(request);
      }
    }
  }
  public IEnumerable<NotFoundRequest> ListRequests()
  {
    return _repo.List();
  }
  // Other methods
}

顯示找不到要求

現在,我有辦法記錄 404年,我需要方法來檢視此資料。若要這樣做,我要建立另一個小型的中介軟體元件的顯示畫面,顯示所有記錄的 NotFoundRequests,按照如何多次已發生。此中介軟體會檢查,查看目前的要求符合特定的路徑,並將略過 (和通過) 任何要求的不相符的路徑。相符的路徑中, 介軟體會傳回一頁,而此表格包含找不到要求中,依頻率。在這裡,使用者將能夠指派個別要求的修正過的路徑,將供未來的要求,而不會傳回 404。

[圖 4 示範有多麼簡單讓 NotFoundPageMiddleware 檢查特定路徑,並進行更新,根據使用的相同路徑的查詢字串值。基於安全性理由,NotFoundPageMiddleware 路徑的存取權應該限制為系統管理員的使用者。

[圖 4 NotFoundPageMiddleware

public async Task Invoke(HttpContext httpContext)
{
  if (!httpContext.Request.Path.StartsWithSegments("/fix404s"))
  {
    await _next(httpContext);
    return;
  }
  if (httpContext.Request.Query.Keys.Contains("path") &&
      httpContext.Request.Query.Keys.Contains("fixedpath"))
  {
    var request = _requestTracker.GetRequest(httpContext.Request.Query["path"]);
    request.SetCorrectedPath(httpContext.Request.Query["fixedpath"]);
    _requestTracker.UpdateRequest(request);
  }
  Render404List(httpContext);
}

撰寫中, 介軟體是硬式編碼路徑 /fix404s 接聽。您最好讓,可以設定,好讓不同的應用程式可以指定他們想任何的路徑。轉譯的清單的所有要求會都依多少 404年的要求顯示它們已記錄,不論是否設定正確的路徑。它不會很難加強提供一些篩選的中介軟體。其他有趣的功能可能會記錄更多詳細的資訊,所以您可以看到哪些重新導向是最受歡迎,或是其中 404年已在過去七天,最常見,但這些讀取器 (或開放原始碼社群) 保留為練習。

[圖 5 顯示的呈現的網頁的外觀範例。

修正 404年頁面
[圖 5 修正 404年頁面

加入選項

我想要能夠指定不同的應用程式內修正的 404年網頁的不同路徑。若要執行這項操作,最好是建立選項類別,然後將它傳遞至使用 DI 的中介軟體。此中介軟體,我建立一個類別,NotFoundMiddlewareOptions,其中包含屬性的值,預設值為 /fix404s 稱為路徑。我可以將此變數傳遞到 NotFoundPageMiddleware IOptions < T >,介面,然後將區域欄位設定為此類型的 Value 屬性。然後您可以更新 /fix404s 我魔法字串參照 ︰

if (!httpContext.Request.Path.StartsWithSegments(_options.Path))

修正 404

當傳入的要求符合具有 CorrectedUrl NotFoundRequest 時,NotFoundMiddleware 應該修改使用 CorrectedUrl 的要求。只更新要求的 [路徑] 屬性可以達成此目的 ︰

string path = httpContext.Request.Path;
string correctedPath = _requestTracker.GetRequest(path)?.CorrectedPath;
if(correctedPath != null)
{
  httpContext.Request.Path = correctedPath; // Rewrite the path
}
await _next(httpContext);

與這個實作中,任何更正的 URL 運作就如同其要求已經變成直接到正確的路徑。然後,繼續要求管線進行,現在使用重寫的路徑。這可能會或可能不是所要的行為。首先,從多個 Url 上編製索引的重複內容的搜尋引擎排名可能會減損。這種方法可能會導致數十個 Url 中的所有對應到相同的基礎應用程式路徑。基於這個理由,建議您最好經常修正 404年使用 (狀態碼 301) 的永久重新導向。

如果我修改傳送重新導向的中介軟體,我可以讓中介軟體的最少運算在此情況下,因為不需要執行任何其餘的管線,如果我決定我只要傳回 301:

if(correctedPath != null)
{
  httpContext.Response. Redirect(httpContext.Request.PathBase + correctedPath +
    httpContext.Request.QueryString, permanent: true);
  return;
}
await _next(httpContext);

請注意不要設定更正會導致無限重新導向迴圈的路徑。

在理想情況下,NotFoundMiddleware 應該支援路徑重寫並永久重新導向。我可以實作此使用我的 NotFoundMiddlewareOptions,讓其中一個或其他設定的所有要求,或可以修改 CorrectedPath NotFoundRequest 路徑上,因此它包含的路徑和要使用的機制。現在我將只更新選項類別,以支援這個行為,並將 IOptions < NotFoundMiddleOptions > 傳入 NotFoundMiddleware 只是因為我已經做為 NotFoundPageMiddleware。在位置選項,重新導向/重寫邏輯會變成 ︰

if(correctedPath != null)
{
  if (_options.FixPathBehavior == FixPathBehavior.Redirect)
  {
    httpContext.Response.Redirect(correctedPath, permanent: true);
    return;
  }
  if(_options.FixPathBehavior == FixPathBehavior.Rewrite)
  {
    httpContext.Request.Path = correctedPath; // Rewrite the path
  }
}

此時,NotFoundMiddlewareOptions 類別具有兩個屬性,其中會列舉 ︰

public enum FixPathBehavior
{
  Redirect,
  Rewrite
}
public class NotFoundMiddlewareOptions
{
  public string Path { get; set; } = "/fix404s";
  public FixPathBehavior FixPathBehavior { get; set; } 
    = FixPathBehavior.Redirect;
}

設定中介軟體

設定您的中介軟體的選項之後,您傳入這些選項的執行個體為中介軟體設定在啟動時。或者,您可以設定繫結選項。ASP.NET 組態很有彈性,可繫結至環境變數,設定檔或以程式設計方式建立。不論其中的設定,就可以繫結選項一行程式碼與組態 ︰

services.Configure<NotFoundMiddlewareOptions>(
  Configuration.GetSection("NotFoundMiddleware"));

與這個之後,我可以藉由更新 appsettings.json (我使用這個執行個體中的組態) 來設定我 NotFoundMiddleware 行為 ︰

"NotFoundMiddleware": {
  "FixPathBehavior": "Redirect",
  "Path": "/fix404s"
}

請注意,轉換為列舉的 FixPathBehavior 字串為基礎的設定檔中的 JSON 值的方式自動的架構。

持續

到目前為止,太好了,一切運作,但不幸的是我的 404年清單和其更正的路徑會儲存在記憶體中集合。這表示,每次我的應用程式重新啟動時,所有這些資料不會遺失。可能是適合我的應用程式會定期重設的 404,其計數,所以我可以更了解哪些是目前最常見,但肯定不想失去我設定的正確的路徑。

幸運的是,我設定 RequestTracker 依賴其持續性 (INotFoundRequestRepository) 的抽象概念,因為它是很容易就能加入將結果儲存在資料庫中使用 Entity Framework 核心 (EF) 的支援。除此之外,我可以輕鬆選擇是否要提供個別的 helper 方法使用 EF 或記憶體中組態 (適合測試項目) 的個別應用程式。

我需要來使用 EF 來儲存和擷取 NotFoundRequests 第一件事是 DbContext。我不想要依賴一個應用程式可能已設定,因此我建立一個只用於 NotFoundMiddleware:

public class NotFoundMiddlewareDbContext : DbContext
{
  public DbSet<NotFoundRequest> NotFoundRequests { get; set; }
  protected override void OnModelCreating(ModelBuilder modelBuilder)
  {
    base.OnModelCreating(modelBuilder);
    modelBuilder.Entity<NotFoundRequest>().HasKey(r => r.Path);
  }
}

DbContext 之後,我需要實作的儲存機制的介面。我建立的要求在其建構函式,NotFoundMiddlewareDbContext 的執行個體,並將它指派給私用欄位,_dbContext EfNotFoundRequestRepository。實作個別的方法很簡單,例如 ︰

public IEnumerable<NotFoundRequest> List()
{
  return _dbContext.NotFoundRequests.AsEnumerable();
}
public void Update(NotFoundRequest notFoundRequest)
{
  _dbContext.Entry(notFoundRequest).State = EntityState.Modified;
  _dbContext.SaveChanges();
}

此時,剩下的就是連結的應用程式的服務容器中的 DbContext 和 EF 儲存機制。這是新的擴充方法 (及重新命名原始的擴充方法,以指出它是 InMemory 版本) ︰

public static IServiceCollection AddNotFoundMiddlewareEntityFramework(
  this IServiceCollection services, string connectionString)
{
    services.AddEntityFramework()
      .AddSqlServer()
      .AddDbContext<NotFoundMiddlewareDbContext>(options =>
        options.UseSqlServer(connectionString));
  services.AddSingleton<INotFoundRequestRepository,
    EfNotFoundRequestRepository>();
  return services.AddSingleton<RequestTracker>();
}

我選擇將連接字串中,傳遞,而不是儲存在 NotFoundMiddlewareOptions,因為大部分使用 EF 的 ASP.NET 應用程式已經提供給它 ConfigureServices 方法中的連接字串。如有需要,呼叫服務時可以使用相同的變數。AddNotFoundMiddlewareEntityFramework(connectionString)。

新的應用程式必須執行才能使用此中介軟體的 EF 版本最後一件事是執行移轉,以確保已正確設定資料庫資料表結構。我需要指定 middelware DbContext,當我這麼做,因為應用程式 (在本例中) 已有它自己的 DbContext。從專案根目錄中執行的命令為 ︰

dotnet ef database update --context NotFoundMiddlewareContext

如果您收到資料庫提供者的相關錯誤,請確定您呼叫服務。AddNotFoundMiddlewareEntityFramework 中啟動。

後續步驟

我已經在這裡顯示的範例可正常運作,並包含記憶體中實作和使用 EF 儲存資料庫中找不到要求計數和固定的路徑。應該保護 404年的清單,並且能夠新增正確的路徑,以便只有系統管理員可以存取它。最後,目前的 EF 實作並不包含任何快取的邏輯,導致應用程式對每個要求所進行的資料庫查詢。基於效能考量,我會加入快取使用 CachedRepository 模式。

此範例的更新的原始碼位於 bit.ly/1VUcY0J


Steve Smith是獨立的訓練、 指導和顧問,以及 ASP.NET 的 MVP。他有貢獻的數十個到正式的 ASP.NET 核心文件的文件 (docs.asp.net),並協助小組快速掌握使用 ASP.NET 的核心。他的連絡 ardalis.com

感謝以下的微軟技術專家對本文的審閱: Chris Ross
Chris Ross 是使用 Microsoft ASP.NET 團隊的開發人員。此時大腦是由中介軟體不足。