【.NET Core MVC】從空白MVC重新理解.NET Core MVC (二)

建立空白MVC專案,利用比對來學習,讓阿猩發現了好多以前從未注意的面向,之前聽到別人說,看書時會去思考,作者表達的脈絡以及出發點是什麼,比對.NET Framework MVC跟.NET Core MVC後,好像開始有點感覺了,兩者的思維好像真的有點不太一樣。

 

wMVC專案初步比較


建立三個新的專案
首先阿猩先建立了下列三個專案(圖1)

  1. .NET Framework MVC (之後都稱Framework)
  2. .NET Core MVC (之後都稱MVC)
  3. .NET Core MVC Empty (之後都稱Empty).
圖1. 各種MVC專案

透過交叉比對發現,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)。

圖2. .NET Framework MVC畫面

接著換MVC的專案,咦?怎麼找不到.sln檔,那就用CLI吧。輸入下列指令:

dotnet run

執行後,請自行開啟網頁並輸入網址,網頁能正常顯示代表程式是沒問題的。最後阿猩以VS開啟資料夾,按下執行鍵發現噴錯了。

圖3. ,NET Core MVC執行時發生的錯誤

除了噴錯之外,一堆功能也不見了,例如

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安裝後,就成功建立囉。

圖4. 未安裝套件所產生的錯誤訊息
圖5. 安裝CodeGeneration.Design

 

以CLI建立其他檔案
Models跟Views我們先不急著建立,但阿猩也測試了建立檔案的指令,touch不行用,故改用echo代替。

echo monkey > Monkey.cs

 

.NET Core主程式


Empty專案會產生下列程式碼來建立主機,程式碼在Program.cs中(圖6),一下子就看完了,然後就馬上暈了...阿哈。

圖6. Program.cs中的程式碼


組態設定
程式看不懂的時候,先看文件準沒錯,如果也看不懂…再說!看完文件1預設建立器設定章節,阿猩大概可以釐清幾個重點:

  1. Host指的就是一台主機,當我們run了MVC專案,會跳出網頁顯示內容,其實是.NET幫我們建立了一台假的主機,同時處理了Web相關的工作。當關掉網頁的時候,也必須把建立的內容回收,否則可能會占用資源(?
  2. 文件中提到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)。

圖7. Startup內的程式碼

 

由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。