2022年7月13日 星期三

EF Core : 使用資料庫反向工程,取得 EF Core 的資料模型

EF Core : 使用資料庫反向工程,取得 EF Core 的資料模型

在使用 Entity Framework Core 進行專案開發的時候,需要先建立好 Entity Framework Core 需要使用的 Model 模型,這樣才能夠與後端的資料庫進行記錄的資料存取需求,而模型是由實體類別和表示與資料庫之會話的內容物件所組成。

一般來說, Entity Framework Core 提供了底下的三種方法來建立要開發用到模型:

  • 透過 Entity Framework Core 提供的工具命令,讀取現有的資料庫綱要 Schema 資訊來產生 EF Core 需要用到模型。
  • 自己手做,將模型手動編寫成符合資料庫的程式碼,這樣子的做法十分刻苦,而且經常會容易出錯,不建議使用。
  • 使用 C# 程式語言進行設計出許多 POCO (Plain Old CLR Object) 類別與 EF Core 會用到的模型之後,透過 EF 移轉 Migration 工具的協助,根據剛剛設計出來的模型資訊,分析並且建立資料庫。 當模型變更時,移轉可讓資料庫同步更新。

在這篇文章中,將會說明第一種的作法,那就是透過 Entity Framework Core 提供的工具命令,讀取現有的資料庫綱要 Schema 資訊來產生 EF Core 需要用到模型的過程與做法。

建立一個測試用的資料庫

為了要練習如何使用資料庫反向工程方式,來建立 EF Core 會用到的模型,在這裡將會建立出底下 ER 模型的資料庫出來,並且該資料庫內也會存在著許多範例紀錄

反向工程練習用資料庫模型

  • 請使用瀏覽器開啟 https://github.com/vulcanlee/Entity-Framework-Core-Getting-Started/blob/main/Database/SchemaAndData.sql 這個網址

  • 在該網頁的最右方中間地方(請參考下圖),會看到一個 [Copy] 複製內容按鈕

  • 請點選這個按鈕,把這裡 SQL 指令複製到剪貼簿內

  • 開啟 Visual Studio 2022 開發工具

  • 請點選右下角的 [不使用程式碼繼續] 的藍色文字

    Visual Studio 2022

  • 現在將進入到 Visual Studio 2022 開發工具內

  • 從功能表中,點選 [檢視] > [SQL Server 物件總管]

    檢視 > SQL Server 物件總管

  • 此時,將會看到 [SQL Server 物件總管] 這個視窗出現在螢幕上

  • 在這個 [SQL Server 物件總管] 視窗內

  • 展開 [SQL Server] 節點

  • 將會看到 [(localdb)\MSSQLLocalDB...] 這個 SQL Server Express LocalDB 這個節點

  • 使用滑鼠右擊這個節點

  • 從彈出功能表中,點選 [新增查詢] 這個選項

  • 此時,將會看到 [SQLQuery1.sql] 視窗出現在螢幕上

  • 將剛剛複製到剪貼簿內的 SQL 指令,貼到這個視窗內

  • 最後,在 [SQLQuery1.sql] 視窗左上方,將會看到一個綠色三角形(請參考上面螢幕截圖紅色箭頭指向地方)

  • 請點選這個綠色三角形按鈕,以便開始執行這個 SQL 指令

  • 很快的,[School] 資料庫已經建立完成了,並且相關資料表內也都有紀錄存在

  • 滑鼠再度右擊這個 [(localdb)\MSSQLLocalDB...] 節點

  • 從彈出功能表中點選 [重新整理] 選項

  • 此時,從 [SQL Server 物件總管] 視窗中,將會看到剛剛建立好的 [School] 資料庫與相關資料表物件

建立 EF Core 需要用到的 School 資料庫模型

為了要能夠建立起 EF Core 能夠用到的模型,可以建立一個 主控台專案 或者 一個 .NET 類別庫;因為,下一篇文章將會說明如何將一個類別庫,打包成為 NuGet 套件,並且上傳到 MyGet 伺服器上,以便可以日後重複使用,所以,在此,將會選擇建立一個 .NET 類別庫的方式

  • 打開 Visual Studio 2022 開發工具

  • 從 Visual Studio 2022 啟動視窗右下方,找到並點選 [建立新新的專案] 這個按鈕

  • 此時,將會看到 [建立新專案] 對話窗出現在螢幕上

  • 請在該對話窗的上方,將會看到三個下拉選單

  • 在最左邊的 [所有語言] 下拉選單中,點取與選擇 [C#] 這個項目

  • 在最右邊的 [所有專案類型] 下拉選單中,點取與選擇 [程式庫] 這個項目

  • 在中間的清單區域,第一個項目將會是 [類別庫] 專案,用於建立以 .NET 或 .NET Standard 為目標的類別庫

  • 請點選這個項目

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

  • 在 [設定新的專案] 對話窗中

  • 找到 [專案名稱] 欄位,在此輸入 DBReverse 作為這個類別庫專案名稱

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

  • 看到 [其他資訊] 對話窗出現後,可以選擇預設選項 [.NET 6 (長期支援)]

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

  • 一旦這個專案建立成功後,在 [方案總管] 中找到並且刪除 [Class1.cs] 檔案

  • 滑鼠右擊 [DBReverse] 專案內的 [相依性] 節點

  • 從彈出功能表中選擇 [管理 NuGet 套件]

  • 搜尋並且安裝 [Microsoft.EntityFrameworkCore.SqlServer] 套件

  • 搜尋並且安裝 [Microsoft.EntityFrameworkCore.Tools] 套件

  • 點選功能表 [工具] > [NuGet 套件管理員] > [套件管理器主控台]

  • 請在 [套件管理器主控台] 視窗內,輸入底下指令

    Scaffold-DbContext "Data Source=(localdb)\MSSQLLocalDB;Initial Catalog=School" Microsoft.EntityFrameworkCore.SqlServer

    套件管理器主控台

  • 底下是執行後的輸出文字

每個封裝均由其擁有者提供授權給您。NuGet 對於協力廠商封裝不負任何責任,也不提供相關的任何授權。某些封裝可能包含須由其他授權控管的相依項目。請遵循封裝來源 (摘要) URL 決定有無任何相依項目。

套件管理員主控台主機版本 6.2.0.146

輸入 'get-help NuGet' 可查看所有可用的 NuGet 命令。

PM> Scaffold-DbContext "Data Source=(localdb)\MSSQLLocalDB;Initial Catalog=School" Microsoft.EntityFrameworkCore.SqlServer
Build started...
Build succeeded.
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.
PM> 

檢視反向工程的執行結果

  • 查看 [方案總管] 將會看到如下圖的畫面

    方案總管

  • 根據最前面所提到的這個 School 資料庫關聯關係圖

    反向工程練習用資料庫模型

  • 可以看的出來,所有在資料庫內的資料表,都有在 [方案總管] 內出現

  • 每個資料表都會對應到一個 C# 類別

  • 例如 對於資料庫上的 [Person] 這個資料表,將會自動產生出一個 [Person.cs] 檔案來對應

  • 底下將會是這個 Person 資料表的 SQL 指令

-- Create the Person table.
IF NOT EXISTS (SELECT * FROM sys.objects 
        WHERE object_id = OBJECT_ID(N'[dbo].[Person]') 
        AND type in (N'U'))
BEGIN
CREATE TABLE [dbo].[Person](
    [PersonID] [int] IDENTITY(1,1) NOT NULL,
    [LastName] [nvarchar](50) NOT NULL,
    [FirstName] [nvarchar](50) NOT NULL,
    [HireDate] [datetime] NULL,
    [EnrollmentDate] [datetime] NULL,
 CONSTRAINT [PK_School.Student] PRIMARY KEY CLUSTERED 
(
    [PersonID] ASC
)WITH (IGNORE_DUP_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
END
GO
  • 底下將會是這個 Person.cs 檔案的 C# 程式碼
public partial class Person
{
    public Person()
    {
        StudentGrades = new HashSet<StudentGrade>();
        Courses = new HashSet<Course>();
    }
    public int PersonId { get; set; }
    public string LastName { get; set; } = null!;
    public string FirstName { get; set; } = null!;
    public DateTime? HireDate { get; set; }
    public DateTime? EnrollmentDate { get; set; }
    public virtual OfficeAssignment OfficeAssignment { get; set; } = null!;
    public virtual ICollection<StudentGrade> StudentGrades { get; set; }
    public virtual ICollection<Course> Courses { get; set; }
}
  • 最後則是最重要的檔案 [SchoolContext.cs]
  • 這是一個繼承 DbContext 類別,這個 DbContext 將會提供與資料庫系統之間的對話,可以用於查詢與儲存記錄從 C# 物件內到資料庫的資料表紀錄裡,原則上,DbContext 是個 Unit Of Work 工作單位 與 Repository 存放酷的設計模式組合而成的。
  • SchoolContext.cs 的程式碼如下所示
using System;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;

namespace DBReverse
{
    public partial class SchoolContext : DbContext
    {
        public SchoolContext()
        {
        }

        public SchoolContext(DbContextOptions<SchoolContext> options)
            : base(options)
        {
        }

        public virtual DbSet<Course> Courses { get; set; } = null!;
        public virtual DbSet<Department> Departments { get; set; } = null!;
        public virtual DbSet<OfficeAssignment> OfficeAssignments { get; set; } = null!;
        public virtual DbSet<OnsiteCourse> OnsiteCourses { get; set; } = null!;
        public virtual DbSet<Outline> Outlines { get; set; } = null!;
        public virtual DbSet<Person> People { get; set; } = null!;
        public virtual DbSet<StudentGrade> StudentGrades { get; set; } = null!;

        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("Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=School");
            }
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Course>(entity =>
            {
                entity.ToTable("Course");

                entity.Property(e => e.CourseId)
                    .ValueGeneratedNever()
                    .HasColumnName("CourseID");

                entity.Property(e => e.DepartmentId).HasColumnName("DepartmentID");

                entity.Property(e => e.Title).HasMaxLength(100);

                entity.HasOne(d => d.Department)
                    .WithMany(p => p.Courses)
                    .HasForeignKey(d => d.DepartmentId)
                    .OnDelete(DeleteBehavior.ClientSetNull)
                    .HasConstraintName("FK_Course_Department");

                entity.HasMany(d => d.People)
                    .WithMany(p => p.Courses)
                    .UsingEntity<Dictionary<string, object>>(
                        "CourseInstructor",
                        l => l.HasOne<Person>().WithMany().HasForeignKey("PersonId").OnDelete(DeleteBehavior.ClientSetNull).HasConstraintName("FK_CourseInstructor_Person"),
                        r => r.HasOne<Course>().WithMany().HasForeignKey("CourseId").OnDelete(DeleteBehavior.ClientSetNull).HasConstraintName("FK_CourseInstructor_Course"),
                        j =>
                        {
                            j.HasKey("CourseId", "PersonId");

                            j.ToTable("CourseInstructor");

                            j.IndexerProperty<int>("CourseId").HasColumnName("CourseID");

                            j.IndexerProperty<int>("PersonId").HasColumnName("PersonID");
                        });
            });

            modelBuilder.Entity<Department>(entity =>
            {
                entity.ToTable("Department");

                entity.Property(e => e.DepartmentId)
                    .ValueGeneratedNever()
                    .HasColumnName("DepartmentID");

                entity.Property(e => e.Budget).HasColumnType("money");

                entity.Property(e => e.Name).HasMaxLength(50);

                entity.Property(e => e.StartDate).HasColumnType("datetime");
            });

            modelBuilder.Entity<OfficeAssignment>(entity =>
            {
                entity.HasKey(e => e.InstructorId);

                entity.ToTable("OfficeAssignment");

                entity.Property(e => e.InstructorId)
                    .ValueGeneratedNever()
                    .HasColumnName("InstructorID");

                entity.Property(e => e.Location).HasMaxLength(50);

                entity.Property(e => e.Timestamp)
                    .IsRowVersion()
                    .IsConcurrencyToken();

                entity.HasOne(d => d.Instructor)
                    .WithOne(p => p.OfficeAssignment)
                    .HasForeignKey<OfficeAssignment>(d => d.InstructorId)
                    .OnDelete(DeleteBehavior.ClientSetNull)
                    .HasConstraintName("FK_OfficeAssignment_Person");
            });

            modelBuilder.Entity<OnsiteCourse>(entity =>
            {
                entity.HasKey(e => e.CourseId);

                entity.ToTable("OnsiteCourse");

                entity.Property(e => e.CourseId)
                    .ValueGeneratedNever()
                    .HasColumnName("CourseID");

                entity.Property(e => e.Days).HasMaxLength(50);

                entity.Property(e => e.Location).HasMaxLength(50);

                entity.Property(e => e.Time).HasColumnType("smalldatetime");

                entity.HasOne(d => d.Course)
                    .WithOne(p => p.OnsiteCourse)
                    .HasForeignKey<OnsiteCourse>(d => d.CourseId)
                    .OnDelete(DeleteBehavior.ClientSetNull)
                    .HasConstraintName("FK_OnsiteCourse_Course");
            });

            modelBuilder.Entity<Outline>(entity =>
            {
                entity.ToTable("Outline");

                entity.Property(e => e.OutlineId).HasColumnName("OutlineID");

                entity.Property(e => e.CourseId).HasColumnName("CourseID");

                entity.Property(e => e.Title).HasMaxLength(100);

                entity.HasOne(d => d.Course)
                    .WithMany(p => p.Outlines)
                    .HasForeignKey(d => d.CourseId)
                    .OnDelete(DeleteBehavior.ClientSetNull)
                    .HasConstraintName("FK_Outline_Course");
            });

            modelBuilder.Entity<Person>(entity =>
            {
                entity.ToTable("Person");

                entity.Property(e => e.PersonId).HasColumnName("PersonID");

                entity.Property(e => e.EnrollmentDate).HasColumnType("datetime");

                entity.Property(e => e.FirstName).HasMaxLength(50);

                entity.Property(e => e.HireDate).HasColumnType("datetime");

                entity.Property(e => e.LastName).HasMaxLength(50);
            });

            modelBuilder.Entity<StudentGrade>(entity =>
            {
                entity.HasKey(e => e.EnrollmentId);

                entity.ToTable("StudentGrade");

                entity.Property(e => e.EnrollmentId).HasColumnName("EnrollmentID");

                entity.Property(e => e.CourseId).HasColumnName("CourseID");

                entity.Property(e => e.Grade).HasColumnType("decimal(3, 2)");

                entity.Property(e => e.StudentId).HasColumnName("StudentID");

                entity.HasOne(d => d.Course)
                    .WithMany(p => p.StudentGrades)
                    .HasForeignKey(d => d.CourseId)
                    .OnDelete(DeleteBehavior.ClientSetNull)
                    .HasConstraintName("FK_StudentGrade_Course");

                entity.HasOne(d => d.Student)
                    .WithMany(p => p.StudentGrades)
                    .HasForeignKey(d => d.StudentId)
                    .OnDelete(DeleteBehavior.ClientSetNull)
                    .HasConstraintName("FK_StudentGrade_Student");
            });

            OnModelCreatingPartial(modelBuilder);
        }

        partial void OnModelCreatingPartial(ModelBuilder modelBuilder);
    } 

} 






2022年7月12日 星期二

C# 非同步 : Task.Run 與 Task.Factory.StartNew 傳入一個 async 非同步委派方法的運作差異

 

Task.Run 與 Task.Factory.StartNew 傳入一個 async 非同步委派方法的運作差異

我們知道在 .NET Framework 4.0 的時候,微軟推出了 TPL (Task Parallel Library) 工作平行程式庫 , 而 TPL 存在的目的是透過 .NET Framework 提供的一組 API,並且將這些 API 組合在一個類別庫內,進而簡化平行處理程式設計的方法與設計流程,讓開發人員更有生產力與可以輕鬆地開發出平行、並行、非同步應用的應用程式。

在 .NET Framework 4.0 的時候,可以透過這個工廠方法 Task.Factory 這個屬性所提供的 TaskFactory.StartNew 方法 來建立並取得一個 工作 Task 物件 (建立並啟動所指定動作委派的工作),代表一個非同步的作業;當使用 StartNew 方法的時候,需要傳入一個委派方法,代表這個非同步作業要執行的工作單元。

當 .NET Framework 進入到 4.5 版本的時候,微軟又推出了 Task.Run 方法 ,根據微軟官方的文件上表示,這個方法將會將指定在 ThreadPool 執行工作排入佇列,並傳回該工作的 Task 或 Task<TResult> 控制代碼。

在此同時,微軟同時在 C# 5.0 也推出了 async 修飾詞 與 await 運算子。其目的在於可以讓 .NET 開發者採用同步的方式開發出有效率的非同步應用程式。

然而,對於 Task.Factory.StartNew 與 Task.Run 這兩個取得非同步工作物件的方法,面對於 async 方法的時候,將會有什麼不同問題呢?

首先,來看看同樣皆為 .NET Framework 4.5 推出的 async , await , Task.Run

底下為使用 Task.Run 取得一個 Task 物件,該非同步工作內將會有一個 async 非同步方法,採用 Lambda 來設計,程式碼如下:

var task = Task.Run(async () =>
{
    Console.WriteLine("非同步工作執行開始"); 
    await Task.Delay(3000);
    Console.WriteLine("非同步工作執行完畢"); 
    return "非同步工作";
});
task.Wait();

當執行這個程式的時候,將會得到底下的執行結果

非同步工作執行開始
非同步工作執行完畢
Press any key for continuing...

從輸出結果可以很清楚的看出,一切的執行結果都如同當初所設計的安排所做到的。

一開始建立一個工作物件,該非同步工作指派了一個 async 的非同步委派方法,因為透過 [Task.Run] 來取得的工作物件,因此,該工作已經將會自動啟動與執行。

接下來將會執行 task.Wait() 方法,採用 Block 封鎖等待的方式,等候這個非同步工作的作業完成,因此,對於當前執行緒而言,將不會繼續往下執行任何程式碼。

當 async 非同步方法休息 3 秒鐘之後,將會執行完畢,因此, task.Wait 這個方法將會解除封鎖,繼續往下執行,因此,將會看到上面的輸出結果。

若在 Visual Studio 2022 開發工具中,將游標移動到 Task.Run 方法上,將會看到下面螢幕截圖,這說明了編譯將推論 Task.Run 這個支援方法,將會回傳一個 Task<string> 這個型別的物件回來,這也正是我們想要得到的結果,也是這個 Lambda 非同步方法真正的回傳結果,代表一個非同步的工作物件,該非同步工作物件將會有一個字串的回傳值。

Task.Run

因此,若將 Task.Run 修改成為 Task.Run<string> ,則編譯器也不會出現任何問題,並且執行結果也是相同的。

var task = Task.Run<string>(async () =>
{
    Console.WriteLine("非同步工作執行開始"); 
    await Task.Delay(3000);
    Console.WriteLine("非同步工作執行完畢"); 
    return "非同步工作";
});
task.Wait();

上面的程式碼將會修改 Task.Run 成為 Task.Factory.StartNew

前者的方法將會存在於 .NET Framework 4.5 之後,而後者是在 .NET Framework 4.0 時候推出的。

底下將會是修改後的程式碼,編譯器將不會有任何錯誤產生

var task = Task.Factory.StartNew(async () =>
{
    Console.WriteLine("非同步工作執行開始"); 
    await Task.Delay(3000);
    Console.WriteLine("非同步工作執行完畢"); 
    return "非同步工作";
});
task.Wait();

現在,執行上述程式碼,將會得到底下結果

非同步工作執行開始
Press any key for continuing...
非同步工作執行完畢

很明顯的,這樣的結果並不是當初設計所期望做到的,當初的設計是

  • 建立與啟動一個非同步工作
  • 採用封鎖式等候這個非同步工作執行完成,才會繼續往下執行

從執行結果可以看出,這個封鎖等待並沒有發生效用,並沒有真正的進行等待,而是繼續往下執行。

現在,來探究究竟發生了什麼問題?

同樣的,在 Visual Studio 2022 工具內,將游標移動到 StartNew 這個方法上,將會看到下圖結果

 Task.Factory.StartNew

這表示編譯器推斷 [Task.Factory.StartNew] 這個方法,將會回傳一個 Task<Task<string>> 型別的物件,簡單來說,將會回傳一個工作物件,而該工作物件的回傳值為另為一個回傳為字串的工作物件。

回顧程式碼,當程式碼執行到 await Task.Delay(3000); 敘述的時候,將會立即 Return,因此,對於這樣回傳Task<Task<string>> 型別值的物件,最外面的 Task 表示已經完成了,因為委派方法已經 Return,而該非同步工作內的 Task<string> 回傳字串的非同步工作物件,卻還沒有執行完成,因此, task.Wait(); 當然會繼續往下執行下去。

現在,將程式碼修改成為底下樣式

var task = Task.Factory.StartNew(async () =>
{
    Console.WriteLine("非同步工作執行開始"); 
    await Task.Delay(3000);
    Console.WriteLine("非同步工作執行完畢"); 
    return "非同步工作";
});
Task<string> innerTask = task.Result;
innerTask.Wait();

把上述的程式碼重新執行一次,將會得到如同 Task.Run 相同的輸出結果,也是我們期望的設計結果

非同步工作執行開始
非同步工作執行完畢
Press any key for continuing...

最後,我想要參考如同 Task.Run 的用法,將這個方法改成 Task.Run<string>

也就是,把  Task.Factory.StartNew 改成  Task.Factory.StartNew<string> , 現在來看看會有什麼結果了,底下將會是修改後的程式碼

var task = Task.Factory.StartNew<string>(async () =>
{
    Console.WriteLine("非同步工作執行開始"); 
    await Task.Delay(3000);
    Console.WriteLine("非同步工作執行完畢"); 
    return "非同步工作";
});
task.Wait();

很不幸的,編譯器將會指出發現一個錯誤,錯誤訊息如下

CS4010	無法將非同步 Lambda 運算式 轉換成委派類型 'Func<string>'。非同步 Lambda 運算式 可能會傳回 void、Task 或 Task<T>,而這些都無法轉換成 'Func<string>'。

請問,你知道為什麼會有這樣的錯誤產生嗎?

提示:從前面的文章可以看出端倪喔