9

任選資料庫存放 ASP.NET Core 靜態檔案

 2 years ago
source link: https://blog.darkthread.net/blog/aspnetcore-static-files-from-db/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
任選資料庫存放 ASP.NET Core 靜態檔案-黑暗執行緒

上回提到 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 好威呀!

and has 2 comments

Comments

# 2022-04-21 05:37 PM by Jeffrey

to Lauyea,這是系統設計的取捨,存在資料庫一定比直接放磁碟慢,但如果方便、彈性是優先要滿足的目標,則效能略差便是要付出的代價。不過,有個迷思要打破:資料容量通常不是 DB 效能不佳關鍵,資料筆數跟索引設計才是,而網路傳輸通常也不是瓶頸,並可透過加入快取機制有效改善。因此,靜態檔移入資料庫影響效能是一定的,除非流量極高,我認為不至嚴重到「疑慮」等級。

Post a comment

Comment
Name Captcha 46 - 15 =

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK