建立空白MVC專案,利用比對來學習,讓阿猩發現了好多以前從未注意的面向,之前聽到別人說,看書時會去思考,作者表達的脈絡以及出發點是什麼,比對.NET Framework MVC跟.NET Core MVC後,好像開始有點感覺了,兩者的思維好像真的有點不太一樣。
wMVC專案初步比較
建立三個新的專案
首先阿猩先建立了下列三個專案(圖1)
- .NET Framework MVC (之後都稱Framework)
- .NET Core MVC (之後都稱MVC)
- .NET Core MVC Empty (之後都稱Empty).
透過交叉比對發現,Framework專案內容物比較多,packages資料夾內有內建的套件,最外層有.sln檔案。MVC專案較乾淨一點,也有Models, Views, Controllers資料夾,資料夾內各自動產生了一個範例。Empty的專案更乾淨,除了MVC的資料夾未建立,wwwroot資料夾也未建立。
而檔案的磁碟大小是
1. Framework: 81.2 MB
2. MVC: 4.21 MB
3. Empty: 20 kb
阿猩先透過PowerShell,在Empty專案中建立後續會使用到的資料夾,上述的資料夾(也可以日後用到再建立啦....)
mkdir Controllers
mkdir Views
mkdir Models
mkdir wwwroot
wwwroot是用於放置網站靜態檔案,.Net Core歸檔及命名方式做了調整,.NTE Framework將css檔案與第三方檔案 (如BootStrap3)放在 Content資料夾。.NET Core 則分別放在css資料夾與js資料夾,其他的第三方庫則放在lib資料夾,這樣看起來乾淨很多。
手動在Empty專案中建立wwwroot內的資料夾
mkdir wwwroot/css
mkdir wwwroot/js
mkdir wwwroot/lib
用Visual Studio開啟專案
點下NETFramework_MVC.sln,開啟後選擇執行,一個預設的網頁就出現了(圖2)。
接著換MVC的專案,咦?怎麼找不到.sln檔,那就用CLI吧。輸入下列指令:
dotnet run
執行後,請自行開啟網頁並輸入網址,網頁能正常顯示代表程式是沒問題的。最後阿猩以VS開啟資料夾,按下執行鍵發現噴錯了。
除了噴錯之外,一堆功能也不見了,例如
1. Ctrl + K Ctrl + C 快速註解功能
2. 插入控制器的選項
3. NuGet剩主控台
當下阿猩心想,X!.NET Core這麼硬,一定要逼使用者寫程式嗎?? 因為這個經驗,也讓阿猩用CLI測試了一段時間,但會有這個問題,其實是阿猩犯蠢了,哈哈。如果讀者也碰到相同問題,這裡就先不說明,讓我們一起練習CLI吧~
以CLI建立Controller
dotnet aspnet-codegenerator controller -name HomeController -outDir Controllers
執行後應該會噴錯,原因是沒有安裝codegenerator(圖4)。使用dotnet add package安裝後,就成功建立囉。
以CLI建立其他檔案
Models跟Views我們先不急著建立,但阿猩也測試了建立檔案的指令,touch不行用,故改用echo代替。
echo monkey > Monkey.cs
.NET Core主程式
Empty專案會產生下列程式碼來建立主機,程式碼在Program.cs中(圖6),一下子就看完了,然後就馬上暈了...阿哈。
組態設定
程式看不懂的時候,先看文件準沒錯,如果也看不懂…再說!看完文件1的預設建立器設定章節,阿猩大概可以釐清幾個重點:
- Host指的就是一台主機,當我們run了MVC專案,會跳出網頁顯示內容,其實是.NET幫我們建立了一台假的主機,同時處理了Web相關的工作。當關掉網頁的時候,也必須把建立的內容回收,否則可能會占用資源(?
- 文件中提到Host.CreateDefaultBuilder的工作為:
(1) 取得目前路徑。
(2) 讀取json檔案: 讀取的資訊用於主機環境參數的設定,所以像appsettings.json或是Properties/ launchSettings.json可別亂刪,因為他們就是用來存放這些參數的。
(3) CreateHostBuilder後,緊跟著的就是Build()跟Run(),在CLI中建置跟執行的關鍵字,也是一樣的。
HostConfiguration
文件2也提到,上述一系列動作叫做組態設定。如果有慢慢看文件1的話,主機組態的章節,其實就呼應上面講的,Host的Configuration,是儲存跟Host有關的設定值,而參數會以json的格式儲存。在每次執行程式前,先決定Configuration是誰,之後再執行build跟run,這樣還有個好處,可以建立好幾個json,後續使用就切換Configuration,來代表不同的Host參數。
appConfiguration
那文件1中提到的應用程式組態又是什麼?來看看Program.cs中另一段程式碼:
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
前面建置好Host的參數,最後使用了UseStartup(),來看看Startup在做什麼(圖7)。
由ConfigureServices初步認識Ioc/DI
Interface
在Empty專案裡,ConfigureServices沒有內容,挖哩…那阿猩要怎麼寫下去...好吧...那先說明另外一件事,你們是否有發現,無論是Program.cs或Startup.cs內的function,輸入的型別都是I開頭的?在.NET中,會在檔案名稱開頭以I來表示Interface,Interface的中文又稱為介面,在嘗試寫ConfigurationServices的內容之前,我們得先認識Interface。
介面就像是共通的規則,提供一個大家通用的介面,例如像插座、像USB,只要先定好統一的規格,應用端只要遵循規格,怎麼應用都可以,例如USB應用在滑鼠、電扇、隨身碟,插座則可以用吸塵器、吹風機等等。寫程式時,會有很多邏輯相同的寫法,較優良的寫法,應該要把相似的段落抽出來,並封裝成類別或介面。我們來做個情境題練習,假設主角是阿猩,我們要寫個讓阿猩吃洋芋片的程式,一開始寫出來的程式應該會像:
public void 阿猩()
{
Chips chips = new Chips();
chips.Eat();
}
public class Chips
{
public void Eat()
{
Console.WriteLine("阿猩吃了洋芋片");
}
}
先建立一個洋芋片的類別,並撰寫吃東西的function,最後在阿猩的程式碼中去呼叫洋芋片,讓我們再增加幾種食物給阿猩。
public class Cookie
{
public void Eat()
{
Console.WriteLine("阿猩吃了餅乾");
}
}
public class Candy
{
public void Eat()
{
Console.WriteLine("阿猩吃了糖果");
}
}
現在的情境有點尷尬,假如阿猩要去Costco大吃特吃,商品上萬樣,我們必須手動建立上萬次類似的程式碼,即使複製貼上,還是有可能不小心手誤去修改到程式碼,例如Eat()打成eat()、Eeat()之類的,而且程式碼重複性質太高,好像有點不太聰明。上面提到,Interface就是介面,以這個例子來看,所有的食物都可以吃,所以可以把「吃」這個動作,轉成用Interface來寫,像下面這樣:
interface IFood
{
void Eat();
}
我們把Eat()抽出來,封裝成IFood,並讓各種食物繼承。
public class Chips : IFood
{
public void Eat()
{
Console.WriteLine("阿猩吃了洋芋片");
}
}
在食物的類別中,滑鼠放在IFood上,提示會告訴你必須實作,因為IFood已經指定要有Eat()這個function,任何繼承IFood的類別,都一定要有Eat()的程式碼,不然就會噴錯。這樣很讚,只要先定義好Interface,即使不同人、不同時間開發,也不會因為命名習慣不同而困擾。到這個步驟,就是控制反轉,不把重複程式碼抽出來,阿猩會被食物給控制,食物想亂命名,阿猩就得配合,例如:
public class Cookie
{
public void Dance()
{
Console.WriteLine("阿猩吃了餅乾");
}
}
阿猩要吃餅乾,必須呼叫Cookie類的別並執行Dance(),這超不合理,難不成阿猩嘴巴說著要去跳舞,但實際上卻偷跑去吃餅乾?阿猩透過Interface定義IFood可以執行哪些動作,食物繼承了IFood就要乖乖聽話,Eat()就只能是Eat(),這樣阿猩才是老大,這樣就把控制權給反轉了。
Dependency Injection(DI)
這樣的程式架構還是有一點小問題,原本在阿猩的程式中呼叫食物時,會是:
Chips chips = new Chips();
如果阿猩每天都要吃不一樣的東西,不就要每天打開這個檔案,然後修改程式碼?所以只有控制反轉是不夠的,雖然我們把控制權交還給阿猩了,但在阿猩程式碼內,阿猩還是太依賴食物了。接著要提到另一個物件導向很好用的觀念,用父類別去裝子類別,當所有食物都繼承了IFood,IFood就是父類別,而食物們就是子類別,所以我們可以改成這樣呼叫食物:
IFood food = new Chips();
那阿猩的程式碼可以改成:
public void 阿猩 (IFood food)
{
food.Eat();
}
這樣今天不管阿猩要吃甚麼,都不用改程式碼了,阿猩只要知道是可以吃的食物,就會收下,如果你不給阿猩食物,給錢應該也會收(誤?
結合控制反轉跟依賴注入,就可以減少重複的程式碼,並更徹底的解耦,也是人家常說的IoC/DI。
以連線字串的方式看Ioc/DI
我們來另一個範例,假設今天要使用EntityFramework(後稱EF)來操作資料庫,呼叫資料庫的時候可能會像這樣:
PubSystem db = new PubSystem();
.NET Framework連線字串取用方式
在阿猩的印象中,在.NET Framework使用EF時,並沒有輸入資料庫連線字串阿,那他是如何連接資料庫的。於是阿猩再回頭去比對Framework跟Core的版本,發現.NET Framework中,EF建立PubSystem時,已經幫我們在PubSystem繼承了DbContext這個類別(DbContext是EF用來連接資料庫的類別),並加上下列程式碼:
public PubSystem()
: base("name=PubSystem")
看到這裡突然恍然大悟,.NET Framework將連線字串記錄在Web.config裡,DbContex這個類別則會幫我們去WebConfig中,找connectionStrings,並看裡面有沒有name="???"。Web.config的內容像:
<connectionStrings>
<add name="PubSystem" connectionString="data source=localhost;initial catalog=pubs;integrated security=True;MultipleActiveResu ltSets=True;App=EntityFramework" providerName="System.Data.SqlClient" />
</connectionStrings>
這樣的作法會有個問題,如果今天筆者手動去改掉name的內容,name不再是PubSystem,DbContext就會抓不到資料庫連線字串了。
.NET Core連線字串取用方式
.NET Core用DI的概念,在.NET Core中,需要在Startup.cs中的ConfigureServices,註冊PubSystem型別的DbContext,其中,透過Lambda表達式執行GetConnectionString()取得連線字串。好難描述阿,直接看程式碼吧:
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<PubSystem>(option => option.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
}
PubSystem的程式碼則改成:
public PubSystem(DbContextOptions<PubSystem> options)
: base(options)
{
}
使用DI的概念,在ConfigureServices註冊,未來我要改連線字串都不是問題,PubSystem只要確認有收到options就好了,是不是跟上面的範例很像?
.NET Core的ConnectionStrings會放在appsettings.json中,長得像是:
{
"ConnectionStrings": {
"DefaultConnection": "Server=localhost;Database=Northwind;Trusted_Connection=True;MultipleActiveResultSets=true"}
}
最後比較一下MVC跟Empty的專案在ConfigureServices中的差異性,發現MVC專案已經使用AddControllersWithViews 註冊了,但empty專案沒有,日後真正用到MVC的功能的時候,需要自己手動加入,但要註冊什麼服務,搭配的關鍵字是什麼,這個就是長時間的功力累積了。而且註冊還會延伸出transient、scoped、singleton生命週期的議題。還是老話一句,學習真的永無止盡,待阿猩有fu整理完再分享吧,下次來說說Startup裡的Configure。