任選資料庫存放 ASP.NET Core 靜態檔案
2 |
上回提到 ASP.NET Core 架構大幅革新,大量以介面取代直接引用類別,並透過依賴注入(DI)取得服務或元件,付出複雜化的代價,為的是在關鍵時刻換取擴充彈性,前篇文章神不知鬼不覺地將 .html、.css、.png 移進 JSON 檔,取代用 wwwroot 存放靜態檔案,便是一個範例。用 JSON 只為方便展示,實質意義不大,將靜態檔案存進資料庫,方便管理換版又能讓多台網站共享,才是我的終極目標。
於是,以 StaticFileJsonProvider 概念為基礎,我又寫了以資料表當作資料來源的版本 - StaticFileDbProvider。
我先設計以下資料表結構:(以 SQLite 示範)
StaticFileIndices 用來放目錄或檔案項目,StaticFileDatas 則存放實際檔案內容,檔案更新時不覆寫舊內容,StaticFileIndices 指向新內容,不清除舊檔案或刪除檔案,只將 StaticFileDatas.Status 改為 'D',並在 Remark 欄位註記何時被哪個使用者、哪個 IP 異動,如有疑義較好追查歷程。
將上回 JSON 的三個檔案存入資料庫,index.html、css/site.css、imgs/logo.gif 分別指向 FileIndexId 4, 2, 3,FileIdexId = -1 的項目是資料夾:
/index.html 更新過一次,第一筆的 Status = 'D',後方 Remark 則有被置換的記錄,第四筆的 Path 與第一筆相同取而代之:
原始碼已放上 Github,實做細節這裡就不花篇幅解釋,基本概念不外乎:定義 Model,宣告 EF Core DbContext,寫 Repository 處理查詢資料夾、刪除資料夾、上傳/更新/刪除及讀取檔案內容等邏輯,實作 IDirectoryContents、IFileInfo、IFileProvider... 想參考的同學可前往 Github 挖掘(若發現 Bug 也請回報給我),我們直接看如何使用它。
網路上查到的 EF Core DbContext 範例大多把 DbContext 寫在 Web 專案裡,我把 StaticFileDbProvider 寫成獨立專案 - Drk.AspNetCore.FileProviders,並設定輸出 Drk.AspNetCore.FileProviders.x.nupkg,實際應用時可透過 NuGet 安裝(已上傳到 NuGet Gallery可直接測試),那這樣要怎麼設定用什麼資料庫?如何建立資料表?
很簡單,在 ASP.NET Core 專案從 NuGet 參考 Drk.AspNetCore.FileProviders,在 Program.cs 裡 AddDbContext() 註冊 StaticFileDbContext,用 UseSqlite() 或 UseSqlServer() 決定要使用的資料庫並加上 b => b.MigrationAssembly("Web專案DLL名稱"),如:options.UseSqlite($"data source={sqliteDbPath}", b => b.MigrationsAssembly("demo-web"));
),然後參考前篇文章介紹的技巧用 doent ef migrations add InitialCreate --context StaticFileDbContext --output-dir Migrations\StatFileProvider
建立 Migrations 程式,再執行 dotnet ef database update --context StaticFileDbContext
或在程式 DbContext.Database.Migrate() 即可建立資料表。元件只提供 EF Core Model 定義,要使用 SQLite、SQL、PostgreSQL 或 Oracle,可依應用程式專案決定,執行 dotnet ef migration add 時會依不用資料庫對映適合的欄位資料表,實現跨資料庫,很酷吧! (註:我實測了 SQLite 及 SQL Server,理論上其他資料庫也可直接使用不需修改,如有問題請再回饋給我)
Github 原始碼裡有個展示用的 DemoWeb 專案,其 Program.cs 如下,預設使用 SQLite:
using System.Text;
using Drk.AspNetCore.FileProviders;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddMemoryCache();
var sqliteDbPath = Path.Combine(builder.Environment.ContentRootPath, "static-files.sqlite");
builder.Services.AddDbContext<StaticFileDbContext>(options =>
{
//options.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Initial Catalog=StaticFiles;Integrated Security=True", b => b.MigrationsAssembly("DemoWeb"));
options.UseSqlite($"data source={sqliteDbPath}", b => b.MigrationsAssembly("DemoWeb"));
});
builder.Services.AddScoped<StaticFileDbRepository>();
var app = builder.Build();
// init db
if (!File.Exists(sqliteDbPath))
{
using (var scope = app.Services.CreateScope())
{
scope.ServiceProvider.GetRequiredService<StaticFileDbContext>()
.Database.Migrate();
}
}
// insert the demo data on-demand
if (app.Configuration.GetValue<string>("insertDemoData", "false") == "true")
{
using (var scope = app.Services.CreateScope())
{
var rsp = scope.ServiceProvider.GetRequiredService<StaticFileDbRepository>();
var userId = "jeffrey";
var clientIp = "::1";
rsp.UpdateFile("/index.html", Encoding.UTF8.GetBytes("<html><body><link href=css/site.css rel=stylesheet /> Web in JSON<img src=imgs/logo.gif /></body></html>"), userId, clientIp);
rsp.UpdateFile("/css/site.css", Encoding.UTF8.GetBytes("body { font-size: 9pt } img { display: block; margin-top: 5px; }"), userId, clientIp);
rsp.UpdateFile("/imgs/logo.gif", Convert.FromBase64String("R0lGODlhSABI...略...IAEWIAGCIABAQA7"), userId, clientIp);
}
}
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new Drk.AspNetCore.FileProviders.StaticFileDbProvider(app.Services),
RequestPath = "/web-in-db"
});
app.MapGet("/", () => Results.Redirect("~/web-in-db/index.html"));
app.Run();
若要用 .NET CLI 一氣喝成,指令如下:
dotnet new web -o DemoWeb
cd DemoWeb
dotnet add package Drk.AspNetCore.FileProviders
dotnet add package Microsoft.EntityFrameworkCore.Design
rem 將 Program.cs 換成上面的程式碼
dotnet ef migrations add InitialCreate --context StaticFileDbContext
dotnet run -- --insertDemoData true
然後,將 Migrations 目錄刪除,修改程式改用 SQL LocalDB :
builder.Services.AddDbContext<StaticFileDbContext>(options =>
{
options.UseSqlServer(
@"Server=(localdb)\mssqllocaldb;Initial Catalog=StaticFiles;Integrated Security=True",
b => b.MigrationsAssembly("DemoWeb"));
//options.UseSqlite($"data source={sqliteDbPath}", b => b.MigrationsAssembly("DemoWeb"));
});
builder.Services.AddScoped<StaticFileDbRepository>();
var app = builder.Build();
// init db
using (var scope = app.Services.CreateScope())
{
scope.ServiceProvider.GetRequiredService<StaticFileDbContext>()
.Database.Migrate();
}
重新執行 dotnet ef migrations add,程式就改在 SQL LocalDB 建立資料表順利運作!
元件只定義 Model、DbContext,至於要用什麼資料庫由你決定,是不是很美妙?
呼口號時間:EF Core 真棒,.NET 好威呀!
A simple implementation to store static files in database for ASP.NET Core.
Comments
# by Lauyea
把圖片存進資料庫裏面共用,會不會有效能的疑慮呢?
# by Jeffrey
to Lauyea,這是系統設計的取捨,存在資料庫一定比直接放磁碟慢,但如果方便、彈性是優先要滿足的目標,則效能略差便是要付出的代價。不過,有個迷思要打破:資料容量通常不是 DB 效能不佳關鍵,資料筆數跟索引設計才是,而網路傳輸通常也不是瓶頸,並可透過加入快取機制有效改善。因此,靜態檔移入資料庫影響效能是一定的,除非流量極高,我認為不至嚴重到「疑慮」等級。