0. 前言
最近剛好有一些空檔, 由 .NET Framework 4.x 跨入 .NET Core 的世界, 目前 .NET Core 最新的 LTS (長期支援版本) 為 .NET 6. 而最簡單的程式, 就是 Console 主控台程式.
因此, 本文將撰寫一支 Console 主控台程式, 利用 Entity Framework Core (以下簡稱 EF Core)的工具, 將資料庫的 Table 轉為 C# 的 class, 並由前述主控台程式, 對資料庫進行讀取.
以下茲分幾個章節, 進行說明.
- 1. 開發及執行環境
- 2. 建立 Console 程式
- 3. 安裝 EF Core 相關套件, 匯入資料庫 Tables, 及探討 Null Reference Types 的議題
- 4. 修改 Console 程式, 讀取資料庫, 並改為非同步版本
- 5. 安裝 Microsoft.Extensions.Configuration 相關套件, 及測試 appsettings.json
- 6. 由 appsettings.json 讀取資料庫連接字串
相關程式 可由此下載.
1. 開發及執行環境
- .NET 6
- Visual Studio 2022
- SQL Server 2017 Developer Edition
- 範例資料庫: Contoso University
可由 {參考文件#07} 取得相關的 SQL Script
2. 建立 Console 程式
如下圖所示, 可以看出, 跟以往的 Console 主控台應用程式有很大的差異, 還好它在 Program.cs 的一開頭, 有留下一個連結, 那就點進去瞧瞧. 點進去以後, 其實就是 {參考文件#01}, 它有說明了, 這個是 .NET 6 新增的 top level statements. 茲將相關的說明, 摘錄於 {參考文件#02} {參考文件#03}
其實它就是一個語法糖, 程式編譯的過程中, 會自動加上 Program class 及 Main() method. 程式裡仍然可以加 methods, 只是會變成 local function, 故不可以有 private, protected, internal, public 等修飾詞
3. 安裝 EF Core 相關套件, 匯入資料庫 Tables, 及探討 Null Reference Types 的議題
3.1 安裝 EF Core 相關套件
打開 Nuget 套件管理器主控台, 依序執行以下指令:
Install-Package Microsoft.EntityFrameworkCore.Tools Install-Package Microsoft.EntityFrameworkCore.SqlServer
Microsoft.EntityFrameworkCore.Tools, 主要用以啟用以下指令. 而 Microsoft.EntityFrameworkCore.SqlServer 係為 Entity Framrwork Core 資料提供者的 Microsoft SQL Server 實作
// -- Add-Migration // -- Bundle-Migration // -- Drop-Database // -- Get-DbContext // -- Get-Migration // -- Optimize-DbContext // -- Remove-Migration // -- Scaffold-DbContext // -- Script-Migration // -- Update-Database
查一下 .csproj, 可以看到納入了 2 個套件, 此與以往 .NET Framework 4.x 採用 packages.config 有所不同.
<ItemGroup> <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="6.0.2" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.2"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> </ItemGroup>
3.2 匯入資料庫 Tables
打開 Nuget 套件管理器主控台, 執行以下指令:
Scaffold-DbContext “Server=.;Database=School;Trusted_Connection=True;” Microsoft.EntityFrameworkCore.SqlServer -ContextDir "Models\Database" -Context "SchoolContext" -OutputDir "Models\Database"
執行結果, 如下截圖. 是有建置成功了. 只是有一個警告, 主因是把資料庫連接字串, 放在 SchoolContext 的程式段裡面. 這裡先跳過, 後面再作調整.
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { if (!optionsBuilder.IsConfigured) { #warning To protect potentially sensitive information in your connection string, you should move it out of source code. You can avoid scaffolding the connection string by using the Name= syntax to read it from configuration - see https://go.microsoft.com/fwlink/?linkid=2131148. For more guidance on storing connection strings, see http://go.microsoft.com/fwlink/?LinkId=723263. optionsBuilder.UseSqlServer("Server=.;Database=School;Trusted_Connection=True;"); } }
3.3 .NET 6 預設強制 nullable reference types
打開 .csproj, 可以看到以下設定, 其中 <Nullable>enable</Nullable> 代表預設強制 Nullable reference types (C# reference), 亦即所有的參考型別, 預設都是不可為 null.
關於 nullable reference types 在 {參考文件#08} 蔡煥麟老師 有詳盡的說明.
<PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>net6.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> </PropertyGroup>
如果參考型別 "有" 特別用 ? 作為 property 的資料型別, 代表 nullable; 例如: public string? EmpName { get; set; } 這就代表 EmpName 被明確定義為 nullable, 此時, 可以通過編譯.
如果參考型別 "沒有" 特別用 ? 作為 property 的資料型別, 代表 not nullable, 例如: public string EmpName { get; set; } 這就代表 EmpName 被明確定義為 not nullable, 此時, 會有編譯警告.
那要如何避開這些編譯警告呢? {參考文件#09} 有提供了詳細說明. 而 Poy Chang 前輩也在 Study4.Tw facebook 社團, 也針對如何避開這些編譯警告, 在 {參考文件#10} 也提供了作法.
- (1). 使用 ? 來明確表示該型別可能有 null 存在
- (2). 使用 default! 來當作預設值
- (3). 使用 null! 當作預設值
- (4). 使用新物件當預設值 (建議),若是實質型別則使用 default 當預設值 (建議)
其中 (2), (3) 的 "!" 代表 null forgiving operator, 它代表的意義是, 這個 variable 或 property 是 not nullable, 告訴編譯器不要顯示警告, 程式設計師會在其它地方賦與 variable 或 property 實際內容值.
但很重要的一點是, 如果程式設計師忘了給值, 則那個 variable 或 property 的內容值, 仍然會是 null. {參考文件#15} 對此有作了探討.
可以參考以下 EF Core 產生的 C# class: StudentGrade. 其中 public virtual Course Course { get; set; } = null!; 所以, 可以避掉編譯警告.
{參考文件#11} 有提到 EF Core 對 nullable reference types 的支援說明.
public partial class StudentGrade { public int EnrollmentId { get; set; } public int CourseId { get; set; } public int StudentId { get; set; } public decimal? Grade { get; set; } public virtual Course Course { get; set; } = null!; public virtual Person Student { get; set; } = null!; }
4. 修改 Console 程式, 讀取資料庫, 並改為非同步版本
STEP 1: 加入 StudentGradeViewModel
internal class StudentGradeViewModel { public int EnrollmentId { get; set; } public int CourseId { get; set; } public int StudentId { get; set; } public decimal? Grade { get; set; } public string StudentName { get; set; } = string.Empty; public string CourseName { get; set; } = string.Empty; public int CourseCredits { get; set; } }
STEP 2: 加入 StudentGradeService, 並 using 相關的 namespaces
其中, GetStudentGrades() 為一個非同步的方法, 用以讀取 table: StudentGrade, 其 Grade 欄位等於 3 的資料
using Microsoft.EntityFrameworkCore; using MyConsoleApp.Models.Database; using MyConsoleApp.ViewModels; internal class StudentGradeService { private readonly SchoolContext _db = new(); public async Task<IEnumerable<StudentGradeViewModel>> GetStudentGrades() { var query = _db.StudentGrades .Where(x => x.Grade == 3) .Select(x => new StudentGradeViewModel() { EnrollmentId = x.EnrollmentId, CourseId = x.CourseId, StudentId = x.StudentId, Grade = x.Grade, StudentName = x.Student.FirstName + " " + x.Student.LastName, CourseName = x.Course.Title, CourseCredits = x.Course.Credits, }); Console.WriteLine($"thread: {Environment.CurrentManagedThreadId} --> before 存取資料庫 ----"); var result = await query.ToListAsync(); Console.WriteLine($"thread: {Environment.CurrentManagedThreadId} --> after 存取資料庫 ----"); return result; } }
STEP 3: 修改 Program.cs, 並 using 相關的 namespaces
using MyConsoleApp.Services; var _service = new StudentGradeService(); var studentGrades = await _service.GetStudentGrades(); foreach (var item in studentGrades) { Console.WriteLine($"thread: {Environment.CurrentManagedThreadId} --> {item.EnrollmentId} | {item.Grade} | {item.StudentName} | {item.CourseName} | {item.CourseCredits}"); }
STEP 4: 觀察一下執行結果
因為採用非同步 (async / await) 的方式, 可以發現, 存取資料庫之前及之後的 thread 是不同的.
thread: 1 --> before 存取資料庫 ---- thread: 10 --> after 存取資料庫 ---- thread: 10 --> 3 | 3.00 | Peggy Justice | Composition | 3 thread: 10 --> 9 | 3.00 | Nino Olivotto | Composition | 3 thread: 10 --> 10 | 3.00 | Nino Olivotto | Literature | 4 thread: 10 --> 16 | 3.00 | Alexandra Walker | Microeconomics | 3 thread: 10 --> 19 | 3.00 | Alexandra Walker | Macroeconomics | 3 thread: 10 --> 26 | 3.00 | Carson Alexander | Microeconomics | 3 thread: 10 --> 29 | 3.00 | Isaiah Morgan | Microeconomics | 3 thread: 10 --> 32 | 3.00 | Candace Kapoor | Physics | 4 thread: 10 --> 34 | 3.00 | Cody Rogers | Physics | 4 thread: 10 --> 35 | 3.00 | Stacy Serrano | Physics | 4
5. 安裝 Microsoft.Extensions.Configuration 相關套件, 及測試 appsettings.json
5.1 安裝 Microsoft.Extensions.Configuration 相關套件
由於 .NET Core 的組態設定, 已不再建議採用 Xml 格式的 app.config, 而是採用 Json 格式的 appsettings.json. 而 .NET Core 也提供了一個 IConfiguration 的介面, 任何的組態設定, 都要實作上述介面.
為了提供對 appsettings.json 的存取, 微軟提供了 Microsoft.Extensions.Configuration 相關套件, 細節如 {參考文件#12}. 包括:
- Microsoft.Extensions.Configuration.Binder: 綁定各個組態來源與 C# 物件的對應
- Microsoft.Extensions.Configuration.Json: 實作來自 JSON 的組態來源 (ex: appsettings.json)
- Microsoft.Extensions.Configuration.EnvironmentVariables: 實作來自環境變數的組態來源
相關的 Nuget 安裝指令如下:
Install-Package Microsoft.Extensions.Configuration.Binder -Version 6.0.0 Install-Package Microsoft.Extensions.Configuration.Json -Version 6.0.0 Install-Package Microsoft.Extensions.Configuration.EnvironmentVariables -Version 6.0.1
5.2 測試 appsettings.json
STEP 1: 加入 appsettings.json
如果找不到 JSON 檔案這個範本, 代表在安裝 Visual Studio 2022 時, 沒有勾選 [V] .NET Framwork 專案與項目範本, 只需用 Visual Studio Installer, 把缺少的這項補上去, 應該就可以看到.
STEP 2: 修改 appsettings.json 的內容如下:
{ "Settings": { "KeyOne": 1, "KeyTwo": true, "KeyThree": { "Message": "這是 [KeyThree] Section 下的項目" } } }
STEP 3: 加入 MyAppSettings 及 NestedSettings 類別, 以與 appsettings.json 對應
namespace MyConsoleApp.Models { internal class NestedSettings { public string Message { get; set; } = null!; } internal class MyAppSettings { public int KeyOne { get; set; } public bool KeyTwo { get; set; } public NestedSettings KeyThree { get; set; } = null!; } }
STEP 4: 撰寫程式讀取內容, 如果讀取出來的中文有亂碼, 請調整 appsettings.json 的編碼為 UTF-8
// ---------------------- // ReadAppSettingsJson // ---------------------- /// <summary> /// 利用 Microsoft.Extensions.Configuration.* 讀取 appsettings.json /// </summary> /// <remarks> /// 這個是 local function, 不可以有 private / public 的修飾字 /// </remarks> void ReadAppSettingsJson() { // 建立 config 物件, 採用 JSON 及 環境收數 提供者, 此時已將 appsettings.json 的內容讀入 IConfiguration config = new ConfigurationBuilder() .AddJsonFile("appsettings.json") .AddEnvironmentVariables() .Build(); // 由 config 物件, 取得已讀入至記憶體的內容, 並填入至 settings 變數 (MyAppSettings 煩別) MyAppSettings settings = config.GetRequiredSection("Settings").Get<MyAppSettings>(); // 呈現結果值 Console.WriteLine($"KeyOne = {settings.KeyOne}"); Console.WriteLine($"KeyTwo = {settings.KeyTwo}"); Console.WriteLine($"KeyThree:Message = {settings.KeyThree.Message}"); // 由 config 物件, 取得已讀入至記憶體的內容, 並填入至 rests 變數 (NestedSettings 煩別) NestedSettings nests = config.GetRequiredSection("Settings:KeyThree").Get<NestedSettings>(); // 呈現結果值 Console.WriteLine($"nests:Message = {nests.Message}"); } ReadAppSettingsJson();
STEP 5: 觀察結果
KeyOne = 1 KeyTwo = True KeyThree:Message = 這是 [KeyThree] Section 下的項目 nests:Message = 這是 [KeyThree] Section 下的項目
6. 由 appsettings.json 讀取資料庫連接字串
終於可以開始處理將資料庫連接字串, 寫死在程式裡的問題了. {參考文件#13} 有提到可繼承自 EF Core 產出的 DbContext 物件, 將資料庫連接字串傳入的解決方式.
STEP 1: 修改 appsettings.json 如下. 主要加入 ConnestionStrings 的設定.
{ "Settings": { "KeyOne": 1, "KeyTwo": true, "KeyThree": { "Message": "這是 [KeyThree] Section 下的項目" } }, "ConnectionStrings": { "SchoolDb": "Server=.;Database=School;Trusted_Connection=True;" }, "SystemName": "Contoso University" }
STEP 2: 將原來發出警告的程式段 (SchoolContext.cs), 作 remark
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { // if (!optionsBuilder.IsConfigured) // { //#warning To protect potentially sensitive information in your connection string, you should move it out of source code. You can avoid scaffolding the connection string by using the Name= syntax to read it from configuration - see https://go.microsoft.com/fwlink/?linkid=2131148. For more guidance on storing connection strings, see http://go.microsoft.com/fwlink/?LinkId=723263. // optionsBuilder.UseSqlServer("Server=.;Database=School;Trusted_Connection=True;"); // } }
STEP 3: 建立新的類別 (ConsoleSchoolContext), 繼承自 SchoolContext
internal class ConsoleSchoolContext : SchoolContext { private readonly string _connectionString; public ConsoleSchoolContext() { var config = new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory()) .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) .Build(); _connectionString = config.GetConnectionString("SchoolDb"); } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { if (!optionsBuilder.IsConfigured) { _ = optionsBuilder .UseSqlServer(_connectionString); } } }
STEP 4: 修改 StudentGradeService.cs 原來以 SchoolContext 建立的 db context, 改用前述的 ConsoleSchoolContext.
//private readonly SchoolContext _db = new(); private readonly ConsoleSchoolContext _db = new();
STEP 5: 觀察執行結果, 應該跟採用 SchoolContext 的結果相同.
thread: 1 --> before 存取資料庫 ---- thread: 4 --> after 存取資料庫 ---- thread: 4 --> 3 | 3.00 | Peggy Justice | Composition | 3 thread: 4 --> 9 | 3.00 | Nino Olivotto | Composition | 3 thread: 4 --> 10 | 3.00 | Nino Olivotto | Literature | 4 thread: 4 --> 16 | 3.00 | Alexandra Walker | Microeconomics | 3 thread: 4 --> 19 | 3.00 | Alexandra Walker | Macroeconomics | 3 thread: 4 --> 26 | 3.00 | Carson Alexander | Microeconomics | 3 thread: 4 --> 29 | 3.00 | Isaiah Morgan | Microeconomics | 3 thread: 4 --> 32 | 3.00 | Candace Kapoor | Physics | 4 thread: 4 --> 34 | 3.00 | Cody Rogers | Physics | 4 thread: 4 --> 35 | 3.00 | Stacy Serrano | Physics | 4
7. 結論
這篇文章有點長, 但終於完成, 也算是往 .NET Core 跨出一小步.
.NET 6 與 .NET Framework 4.x 的差異真的很大, 除了 C# 語法的快速演進, 專案範本也有差異, 需要很大的努力, 才能追上, 套句 Bill 老師在 twMVC#36: C#的美麗與哀愁 提到 "C# 的美麗, 是工程師們的哀愁" {參考文件#14}. 真的蠻貼切的.
8. 參考文件
- 01. Microsoft Docs, New C# templates generate top-level statements
提到了 .NET 6 top level statements 新功能, 主要說明其原理及最後產出的程式內容. 涵蓋 02, 03 這 2 篇參考文件段落
- 02. Microsoft Docs, New C# templates generate top-level statements: Use the new program style
.NET 6 的 Console 範本, 看不到 Program class 及 Main() method, 其實就是一個語法糖, 程式編譯的過程中, 會自動加上 Program class 及 Main() method. 程式裡仍然可以加 methods, 只是會變成 local function, 故不可以有 private, protected, internal, public 等修飾詞
- 03. Microsoft Docs, New C# templates generate top-level statements: Use the old program style
.NET 5 及之前 的 Console 範本, 有 Program class 及 Main() method, 可以像以往寫程式.
- 04. Microsoft Docs, Top-level statements - programs without Main methods
主要描述在各種狀況下, 程式編譯過程中, 會產生什麼樣的 Program class 及 Main() method
Top-level code contains Implicit Main signature await and return static async Task<int> Main(string[] args) await static async Task Main(string[] args) return static int Main(string[] args) No await or return static void Main(string[] args) - 05. The Will Will Web, 使用 .NET Generic Host 建立 Console 主控台應用程式 (.NET Core 3.1+)
描述如何利用 Generic Host 來建立 Console 程式.
[摘自原文] 使用 .NET Generic Host 有個非常棒的地方, 就是他可以幫你處理優雅的關閉(graceful shutdown) 應用程式. 這個特性特別適用於容器化應用程式執行, 當你的應用程式部署在 Docker 或 Kubernetes 環境下, 當容器需要被關閉或重啟時,會向應用程式送出一個 SIGINT 或 SIGTERM 訊號,這就如同你對應用程式按下 Ctrl+C 中斷程式執行一樣。 - 06. Danylko Web, Collection: SQL Server Sample Databases
彙整 SQL Server 相關範例資料庫的連結, 包括: NorthWind, AdventureWorks, Contoso University
- 07. Microsoft Docs, School Sample Database
內含建立 Contoso University 範例資料庫的 SQL Script
- 08. Huanlin 學習筆記, 初探 C# 8 的 Nullable Reference Types
針對 Nullable Reference Types 有詳盡的說明
- 09. Microsoft Docs, Learn techniques to resolve nullable warnings
內含對於 null forgiving operator 的說明
- 10. Study4.TW: Poy Chang, NET 6 的專案預設都會開啟 Nullable 檢查
內含如何處理 Non-nullable property 'Name' must contain a non-null value when exiting constructor. Consider declaring the property as nullable. 這類編譯警告訊息的作法
- 11. Microsoft Docs, Working with Nullable Reference Types
這篇有提到 EF Core 對 nullable reference types 的支援說明
- 12. Microsoft Docs, Configuration in .NET
可以參考 Basic Example 那個章節, 進行安裝
- 13. StackOverflow, How to use proper connection string .net core console application
有提到如何在 .NET Core 主控台程式中, 讀取 appsettings.json 的資料庫連接字串, 建立自訂的 DbContext
- 14. Bill Chung, twMVC#36: C#的美麗與哀愁
C# 的美麗, 是工程師們的哀愁. 走過 C# 1.0 到 C# 8.0 的漫長歲月.
- 15. StackOverflow, What does null! statement mean?
有針對 null forgiving operator 作了討論, 並有範例可供參考
沒有留言:
張貼留言