2021年7月21日 星期三

ASP.NET Core 應該具備知識 - 相依性注入設計模式

ASP.NET Core 應該具備知識 - 相依性注入設計模式



建立一個 ASP.NET Core MVC 專案

  • 開啟 Visual Studio 2019

  • 在 [Visual Studio 2019] 對話窗中,點選右下方的 [建立新的專案] 選項

  • 在 [建立新專案] 對話窗中,在中間上方的專案範本過濾條件中

    1. 設定程式語言為 [C#]
    2. 設定專案範本為 [Web]
    3. 選擇專案範本項目清單,點選 [ASP.NET Core Web API] 這個專案範本項目
    4. 點選右下方的 [下一步] 按鈕

  • 在 [設定新的專案] 對話窗出現後

    在 [專案名稱] 內,輸入 AC03

    點選右下角的 [下一步] 按鈕

  • 在 [其他資訊] 對話窗出現後,確認 [目標 Framework] 的下拉選單要選擇 [.NET 5.0 (目前)]

  • 點選右下角的 [建立] 按鈕

  • 此時這個 [ASP.NET Core Web API] 專案已經建立完成,從方案總管視窗內可以看到如下圖的結構

建立服務類別與介面

  • 滑鼠右擊專案節點

  • 從彈出功能表來選取 [加入] > [新增資料夾]

  • 將這個資料夾命名為 Services

  • 滑鼠右擊剛剛建立的 [Services] 資料夾

  • 從彈出功能表點選 [加入] > [類別]

  • 當出現 [新增項目 - AC03] 對話窗

  • 在中間區域的樣板清單項目,點選 [介面] 項目

  • 在下方 [名稱] 欄位輸入 [IMyService.cs]

  • 在右下方點選 [新增] 按鈕

  • 使用底下的程式碼替換掉這個檔案原先內容

using System;

namespace AC03.Services
{
    public interface IMyService
    {
        Guid Guid { get; set; }
        int HashCode { get; }
    }
    public interface IMyServiceTransient : IMyService { }
    public interface IMyServiceScoped : IMyService { }
    public interface IMyServiceSingleton : IMyService { }
}

為了要能夠充分表現出 .NET Core 中的相依性插入 所支援相依性注入的三中生命週期,在這個檔案中,將會宣告四個介面

其中 [IMyService] 為基底介面的宣告,在這裡宣告會有兩個屬性,一個是型別為 [Guid] 的唯一識別碼屬性,另外一個透過 .NET 本身的 [object] 物件的 [GetHashCode] 方法來目前物件的雜湊碼;在這裡是要透過這兩個屬性來判斷出當時注入的物件,是否為同一個

[IMyServiceTransient] 介面將會是用來取得注入 Transient 暫時性 的物件,這代表了每次透過相依性注入機制來取得或者注入一個物件的時候,相依性注入的 Container 容器都會每次重新建立出一個新的物件出來;這些注入得到的物件當沒有使用後,將會被記憶體回收機制回收。

[IMyServiceScoped] 介面將會是用來取得注入 Scoped 範圍 的物件,當使用相依性注入容器來註冊具有範圍性的後,每次需要注入這樣類型物件的時候,會判斷當時若有使用 HTTP Request 請求,對於同一個 HTTP 請求,第一次將會產生出一個新的注入物件,而在同一個 HTTP 請求想要注入這樣具有範圍性的物件,將不再建立新的物件,相依性注入容器而是將剛剛剛產生的物件直接回傳回去,因此,在同一個 HTTP Request 請求要注入具有範圍性的物件,將會取得相同的物件;這些注入得到的物件當同一個 HTTP 請求內都沒有使用後,將會被記憶體回收機制回收。

[IMyServiceSingleton] 介面將會是用來取得注入 Singleton 單一 的物件,這代表了每次透過相依性注入機制來取得或者注入一個物件的時候,相依性注入的 Container 容器僅僅會在第一次的時候來建立出一個新的物件出來,之後同樣的 單一 注入物件請求,大家都會取得同一個物件。

  • 滑鼠右擊 [Services] 資料夾
  • 從彈出功能表點選 [加入] > [類別]
  • 在下方 [名稱] 欄位輸入 [MyService.cs]
  • 在右下方點選 [新增] 按鈕
  • 使用底下的程式碼替換掉這個檔案原先內容
using System;

namespace AC03.Services
{
    public class MyService : IMyService, IMyServiceTransient, IMyServiceScoped, IMyServiceSingleton
    {
        public int HashCode
        {
            get
            {
                return this.GetHashCode();
            }
        }
        public Guid Guid { get; set; } = Guid.NewGuid();
    }
}

在這裡很簡單的實作這兩個屬性,只是分別宣告其預設值。

進行相依性服務的註冊 Registration

  • 在專案根目錄下找到並且打開 [Startup.cs] 檔案
  • 找到 [ConfigureServices] 方法,在這個方法內加入底下的程式碼
#region 註冊客製化的服務
services.AddTransient<IMyServiceTransient, MyService>();
services.AddScoped<IMyServiceScoped, MyService>();
services.AddSingleton<IMyServiceSingleton, MyService>();
services.AddTransient<MyService>();
#endregion

在這裡分別使用不同的介面來對相依性注入容器宣告都要注入同一個類型的物件,甚至在最後一行的程式碼,也可以直接使用實體類別來進行註冊,不需要使用到介面。

開始使用 IoC Container 反轉控制容器 來注入物件

  • 滑鼠右擊 [Controllers] 資料夾

  • 從彈出功能表清單中點選 [加入] > [控制器]

  • 當 [新增 Scaffold 項目] 對話窗出現之後

  • 依序點選 [已安裝] > [通用] > [API] > [API 控制器 - 空白]

  • 點選右下角的 [加入] 按鈕

  • 現在將會看到 [新增項目 - AC03] 對話窗

  • 在下方的 [名稱] 欄位中輸入 DIController

  • 點選右下方 [新增] 按鈕

  • 使用底下的程式碼替換這個檔案內容

using AC03.Services;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Text;

namespace AC03.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class DIController : ControllerBase
    {
        private readonly IMyServiceTransient myServiceTransient1;
        private readonly IMyServiceTransient myServiceTransient2;
        private readonly IMyServiceScoped myServiceScoped1;
        private readonly IMyServiceScoped myServiceScoped2;
        private readonly IMyServiceSingleton myServiceSingleton1;
        private readonly IMyServiceSingleton myServiceSingleton2;
        private readonly MyService myService;

        public DIController(IMyServiceTransient myServiceTransient1, IMyServiceTransient myServiceTransient2,
            IMyServiceScoped myServiceScoped1, IMyServiceScoped myServiceScoped2,
            IMyServiceSingleton myServiceSingleton1, IMyServiceSingleton myServiceSingleton2, MyService myService)
        {
            this.myServiceTransient1 = myServiceTransient1;
            this.myServiceTransient2 = myServiceTransient2;
            this.myServiceScoped1 = myServiceScoped1;
            this.myServiceScoped2 = myServiceScoped2;
            this.myServiceSingleton1 = myServiceSingleton1;
            this.myServiceSingleton2 = myServiceSingleton2;
            this.myService = myService;
        }

        [HttpGet]
        public string Get()
        {
            StringBuilder result = new StringBuilder();

            result.Append($"注入 [類別] 暫時性物件1 " + Environment.NewLine);
            result.Append($"Guid:{myService.Guid} , HashCode :{myService.HashCode}" +
                Environment.NewLine);
            result.Append($"注入暫時性物件1 " + Environment.NewLine);
            result.Append($"Guid:{myServiceTransient1.Guid} , HashCode :{myServiceTransient1.HashCode}" +
                Environment.NewLine);
            result.Append($"注入暫時性物件2 " + Environment.NewLine);
            result.Append($"Guid:{myServiceTransient2.Guid} , HashCode :{myServiceTransient2.HashCode}" +
                Environment.NewLine + Environment.NewLine);
            result.Append($"注入範圍性物件1 " + Environment.NewLine);
            result.Append($"Guid:{myServiceScoped1.Guid} , HashCode :{myServiceScoped1.HashCode}" +
                Environment.NewLine);
            result.Append($"注入範圍性物件2 " + Environment.NewLine);
            result.Append($"Guid:{myServiceScoped2.Guid} , HashCode :{myServiceScoped2.HashCode}" +
                Environment.NewLine + Environment.NewLine);
            result.Append($"注入永久性物件1 " + Environment.NewLine);
            result.Append($"Guid:{myServiceSingleton1.Guid} , HashCode :{myServiceSingleton1.HashCode}" +
                Environment.NewLine);
            result.Append($"注入永久性物件2 " + Environment.NewLine);
            result.Append($"Guid:{myServiceSingleton2.Guid} , HashCode :{myServiceSingleton2.HashCode}" +
                Environment.NewLine);

            return result.ToString();
        }
    }
}

在這個控制器類別中,可以看到使用這樣的建構函式 public DIController(IMyServiceTransient myServiceTransient1, IMyServiceTransient myServiceTransient2,IMyServiceScoped myServiceScoped1, IMyServiceScoped myServiceScoped2,IMyServiceSingleton myServiceSingleton1, IMyServiceSingleton myServiceSingleton2, MyService myService) 宣告,這代表了當產生了 [DIController] 控制器物件,將會透過 相依性注入容器 Dependency Injection Container 或者 反轉控制容器 Inversion of Control IoC Container 來依據當時註冊的生命週期設定,注入適當的物件到建構函式內,在建構式內將會把這些注入的參數指派給該類別的欄位。

執行這個專案

  • 請按下 [F5] 按鍵,開始執行這個專案

  • 瀏覽器的畫面如下

  • 請在瀏覽器網址列上輸入 https://localhost:5001/api/di

從這個 [DIController] 控制器提供的 [Get] 方法執行結果看出

注入 [類別] 暫時性物件1 
Guid:925f5a0a-f899-47d0-9321-21801eb9619c , HashCode :47546512

在進行相依性注入設計過程中,在進行註冊的過程中,是可以直接類別,而不一定要透過介面,上面的執行結果看到直接注入一個類別的執行結果

接下來要來看看採用暫時性生命週期的注入方式所執行的結果,結果如下

注入暫時性物件1 
Guid:674f897d-5c92-4d41-b45a-287b455ece2e , HashCode :48077747
注入暫時性物件2 
Guid:c841ca16-ed9e-424e-82a6-4db695f4c828 , HashCode :6999374

從上面看到的執行結果,在同一個 HTTP 請求中,使用 [IMyServiceTransient] 來注入兩個物件,從 [HashCode] 與 [Guid] 看出這兩個注入的物件為不同的物件。

現在來看看具範圍性的注入行為

注入範圍性物件1 
Guid:97bd9b84-89e9-4971-924f-6a03162fe68b , HashCode :7425030
注入範圍性物件2 
Guid:97bd9b84-89e9-4971-924f-6a03162fe68b , HashCode :7425030

上面為採用範圍性的注入物件,從 [HashCode] 與 [Guid] 看出這兩個注入的物件為指向同一個物件,這裡可以看出與短暫性注入的方式不同。

最後來看看單一性的注入行為

注入永久性物件1 
Guid:990251da-cf95-4894-8867-c9672b1cfc99 , HashCode :28282175
注入永久性物件2 
Guid:990251da-cf95-4894-8867-c9672b1cfc99 , HashCode :28282175

乍看之下,採用單一性的注入得到的物件似乎與具範圍性方式注入的物件得到相同的結果,因為,所注入的兩個單一性注入物件,都指向同一個物件。

現在請重新整理呼叫這個 [Get] Web API 網頁,將會得到下圖畫面

從第二次的執行結果可以看到,在注入範圍性物件將會得到底下的結果

注入範圍性物件1 
Guid:718fe4fa-f69e-4b06-b023-f8a87b36408c , HashCode :65472899
注入範圍性物件2 
Guid:718fe4fa-f69e-4b06-b023-f8a87b36408c , HashCode :65472899

這與第一次呼叫這個 Web API 所得到物件將會不同(可以從 Hash Code 或者從 Guid 的物件值簡單判斷出來,會有這樣的結果那是因為這是兩個不同的 HTTP 請求,因此,對於具有範圍性的相依性注入行為發生的時候,會在這次新的 HTTP 請求發生的時候,將會產生新的物件。

最後,對於第二次呼叫單一性的注入行為,得到底下的結果

注入永久性物件1 
Guid:990251da-cf95-4894-8867-c9672b1cfc99 , HashCode :28282175
注入永久性物件2 
Guid:990251da-cf95-4894-8867-c9672b1cfc99 , HashCode :28282175

從這裡便可以看出 [範圍性] 相依性注入 與 [單一性] 相依性注入的差異,在採用單一性注入的時候,不論在任何時候(只要這個應用程式沒有重新啟動),當使用單一性注入請求,將會得到相同的物件,就算在不同的 HTTP 請求發生的時候,不會得到不同的物件











2021年7月20日 星期二

ASP.NET Core 應該具備知識 - 靜態檔案

ASP.NET Core 應該具備知識 - 靜態檔案


ASP.NET Core 應該具備知識

ASP.NET Core 應用程式啟動與 Startup 類別

ASP.NET Core 靜態檔案 UseStaticFiles

ASP.NET Core 相依性注入設計模式 Dependency Injection

ASP.NET Core 中介軟體 Middleware

ASP.NET Core 設定 Configuration

ASP.NET Core 選項模式 IOptions

ASP.NET Core 日誌記錄 Logger

ASP.NET Core 從空白專案建立 Blazor 應用

建立一個 空白 ASP.NET Core 專案

首先來建立一個測試用的專案,這裡使用的是 [空白 ASP.NET Core]

  • 開啟 Visual Studio 2019

  • 在 [Visual Studio 2019] 對話窗中,點選右下方的 [建立新的專案] 選項

  • 在 [建立新專案] 對話窗中,在中間上方的專案範本過濾條件中

    1. 設定程式語言為 [C#]
    2. 設定專案範本為 [Web]
    3. 選擇專案範本項目清單,點選 [空白 ASP.NET Core] 這個專案範本項目
    4. 點選右下方的 [下一步] 按鈕
  • 在 [設定新的專案] 對話窗出現後

    在 [專案名稱] 內,輸入 AC02

    點選右下角的 [下一步] 按鈕

  • 在 [其他資訊] 對話窗出現後

  • 確認 [目標 Framework] 的下拉選單要選擇 [.NET 5.0 (目前)]

  • 點選右下角的 [建立] 按鈕

  • 此時這個 [ASP.NET Core] 專案已經建立完成

  • 從方案總管視窗內可以看到如下圖的結構

在專案中存放靜態圖片檔案

靜態檔案會儲存在專案的 web 根目錄 中,而 Web 根目錄 表示公用靜態資源檔的基底路徑,在這個目錄下可以存放如:樣式表單 (.css)、JavaScript (.js)、影像 (.png、 .jpg) 等類型的檔案。

不過,在這個 [] 類型的專案內,似乎沒有看到這樣的目錄存在,因此,可以透過底下程序來建立起來

  • 滑鼠右擊專案節點

  • 從彈出功能表中點選 [加入] > [新增資料夾]

  • 將這個資料夾命名為 wwwroot

  • 從上面截圖可以看到在 [方案總管] 視窗內出現了一個新的 [wwwroot] 資料夾,不過,該資料夾的代表圖示變成一個 [地球] 圖示,這代表了這個資料夾為 [Web 根目錄]

  • 請找到一個圖片檔案,這裡找到一個 [me.jpg] 圖片檔案,請使用 [檔案總管] 拖拉到 [me.jpg] 圖片檔案到 [方案總管] 內的 [wwwroot] 資料夾

執行這個專案

  • 請按下 [F5] 按鍵,開始執行這個專案

  • 請輸入 https://localhost:5001/me.jpg 服務端點

  • 可是卻沒有看到圖片,瀏覽器的畫面如下

  • 這裡出現了 HTTP 404 的錯誤代碼,代表找不到這個圖片資源檔案

加入使用靜態資源檔案的中介軟體

  • 剛剛明明有加入 [wwwroot] 資料夾與圖片檔案,但是卻沒有從瀏覽器看到圖片
  • 在專案根目錄中打開 [Startup.cs] 檔案
  • 找到 [Configure] 方法
  • 將這個 app.UseStaticFiles(); 敘述加入到 app.UseRouting(); 敘述前
  • 底下為完成後的 [Configure] 方法
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    #region 啟用靜態檔案服務
    app.UseStaticFiles();
    #endregion

    app.UseRouting();
    app.UseEndpoints(endpoints =>
    {
        endpoints.MapGet("/", async context =>
        {
            await context.Response.WriteAsync("Hello World!");
        });
    });
}

再度執行這個專案

  • 請按下 [Ctrl] + [F5] 按鈕,停止執行這個專案

  • 請按下 [F5] 按鍵,開始執行這個專案

  • 請輸入 https://localhost:5001/me.jpg 服務端點

  • 現在卻可以看到圖片,瀏覽器的畫面如下

 










2021年7月19日 星期一

Blazor Server 必會開發技能 - 表單和驗證

Blazor Server 必會開發技能 - 表單和驗證 


Blazor Server 必會開發技能

Blazor Server 建立一個新的頁面

Blazor Server 元件生命週期事件 Component Life Cycle

Blazor Server C# 程式碼設計方法

Blazor Server 單向資料綁定 One Way Data Binding 與 重新轉譯 Binding

Blazor Server Hello 互動頁面與事件設計 Two Way Data Binding

Blazor Server 元件間的參數傳遞與回應事件 Component Parameter EventCallback

Blazor Server C# 與 JavaScript 互相呼叫 IJSRuntime

Blazor Server 表單和驗證 Form Validation 

對於在進行元件化設計的時候,將會有這些元件間互相傳遞資訊的需求,在這個練習 Blazor 專案內,將會設計一個子元件,這個子元件將會在 Index.razor 元件內會參考使用。

上層元件 (Index.razor) 提供一個按鈕,將會產生一個現在日期時間字串,透過綁定子元件參數的方式,把這個物件值傳遞到子元件內;而在子元件內會有一個文字輸入盒與一個按鈕,使用者在子元件內輸入任何文字到文字輸入盒內,接著按下按鈕,此時,會從子元件內把剛剛輸入的文字,傳遞到 Index.razor 元件內來顯示在網頁上。

這裡說明的範例專案原始碼位於 BS08

建立 Blazor Server-Side 的專案

  • 打開 Visual Studio 2019

  • 點選右下方的 [建立新的專案] 按鈕

  • [建立新專案] 對話窗將會顯示在螢幕上

  • 從[建立新專案] 對話窗的中間區域,找到 [Blazor 應用程式] 這個專案樣板選項,並且選擇這個項目

  • 點選右下角的 [下一步] 按鈕

  • 現在 [設定新的專案] 對話窗將會出現

  • 請在這個對話窗內,輸入適當的 [專案名稱] 、 [位置] 、 [解決方案名稱]

    在這裡請輸入 [專案名稱] 為 BS08

  • 完成後,請點選 [建立] 按鈕

  • 當出現 [建立新的 Blazor 應用程式] 對話窗的時候

  • 請選擇最新版本的 .NET Core 與 [Blazor 伺服器應用程式]

  • 完成後,請點選 [建立] 按鈕

    稍微等會一段時間,Blazor 專案將會建立起來

修改參考子元件的 Index.razor 元件

  • 打開 [Pages] 資料夾內的 [Index.razor] 檔案
  • 請使用底下程式碼替換到這個檔案內容
@page "/"
@using System.ComponentModel.DataAnnotations

<h1>Hello, 表單和驗證!</h1>

<EditForm Model="@User" OnValidSubmit="@HandleValidSubmit">
    <DataAnnotationsValidator />
    <ValidationSummary />

    <div class="form-row">
        <div class="form-group col">
            <label class="control-label">姓名</label>
            <input class="form-control" @bind="@User.Name"
                   Placeholder="請輸入姓名" />
            <ValidationMessage For="@(() => User.Name)" />
        </div>
    </div>

    <div class="form-row">
        <div class="form-group col">
            <label class="control-label">年紀</label>
            <input class="form-control" @bind="@User.Age"
                   Placeholder="請輸入Age" />
            <ValidationMessage For="@(() => User.Age)" />
        </div>
    </div>

    <button class="btn btn-primary" type="submit">Submit</button>
</EditForm>

<div class="display-1 text-success">@Message</div>
@code{
    public UserData User { get; set; } = new();
    public string Message { get; set; }
    void HandleValidSubmit()
    {
        Message = $"{User.Name} , {User.Age}";
    }

    public class UserData
    {
        [Required(ErrorMessage = "姓名不可為空白")]
        public string Name { get; set; }
        [Required(ErrorMessage = "年紀不可為空白")]
        [Range(minimum:0, maximum:120, ErrorMessage ="年紀數值必須介於 0~120")]
        public string Age { get; set; }
    }
}

在 Blazor 開發框架下,提供了 EditForm 類別 元件,該元件在進行轉譯過程可以將 form 元素的相關應用。 在這個範例中,提供了 [Model] 參數屬性,在這裡可以指定表單的最上層模型物件與針對此模型來建立編輯內容,而 [OnValidSubmit] 參數將會綁定一個方法,當進行提交表單的時候,若沒有違反表單驗證規則,這個委派方法將會被呼叫。

在 [EditForm] 內加入了兩個元件 <DataAnnotationsValidator /> 與 <ValidationSummary />。 [DataAnnotationsValidator] 是提供將資料批註驗證支援加入至 EditContext,而 [ValidationSummary] 則是將違反表單驗證規則的所有訊息都集中顯示在這裡。

在這個 [EditForm] 區段內將會有兩個 <div class="form-row"> ... </div> 宣告,分別使用了 <input class="form-control" @bind="@User.Name" Placeholder="請輸入姓名" /> 與 <input class="form-control" @bind="@User.Age" Placeholder="請輸入Age" /> HTML 標記,使用了雙向資料綁定提供使用者來輸入 姓名 與 年紀,其中每個文字輸入盒下方還有宣告 <ValidationMessage For="@(() => User.Name)" /> ,這表示若這個欄位違反資料驗證的設定,將會顯示驗證訊息。

對於綁定的資料模型,這裡使用的是 [UserData],這個類別內有兩個屬性,分別有使用 [Required] 與 [Range] 屬性宣告,定義這些屬性資料要具備甚麼樣的格式。

執行這個專案

  • 按下 [F5] 按鍵,開始執行這個 Blazor 專案

  • 一旦啟動完成,就會自動開以瀏覽器

  • 請點選 [Submit] 按鈕

  • 因為現在都沒有輸入任何資料,所以,違反資料驗證宣告,因此,將會將違反表單資料驗證的訊息,使用紅色的文字在網頁上

  • 一旦輸入的文字內容都正確的話,就會出現如下圖畫面