Entity Framework Core (EF Core) 是一個提供了實體、關係映射的架構,通過它們,可以創建映射到資料庫的類型,使用 LINQ 創建資料庫查詢、新增和更新物件,把它們寫入資料庫。
Entity Framework 經過多年的改變,Entity Framework Core (EF Core) 已完全重寫了。不僅可以在 Windows 上使用,也可以在 Linux 和 Mac 上使用。他支持關聯式資料庫和 NoSQL 數據存儲。
這裡要使用 EF Core 建立一個簡單的模型讀寫來自 Oracle 資料庫 DEPT 與 EMP 資料表的資料,所以這裡會有 Dept 與 Emp 兩個類型映射到 Oracle 資料庫的這兩個資料表。把資料寫入資料庫,讀取、更新和刪除它們。
我使用 Visual Studio Code 開發,所以你的機器必須安裝 .NET Core SDK 。你如果要使用 EF Core 的反向工程 ,則還必須安裝Entity Framework Core 工具參考 - .NET CLI 。
反向工程可以幫你把資料庫的資料表(Table)產生 C# 的類型與映射,開始先不談反向工程,以免失焦及複雜化,後面再來補充說明。
開啟 VS Code 建立一個專案。
$ > dotnet --version 3.1.201 $ > dotnet new console --name OracleEFCoreSample The template "Console Application"  was created successfully. Processing post-creation actions... Running 'dotnet restore'  on OracleEFCoreSample\OracleEFCoreSample.csproj...   I:\u01\projects\OracleEFCoreSample\OracleEFCoreSample.csproj 的還原於 238.4 ms 完成。 Restore succeeded. $ > cd  OracleEFCoreSample $ OracleEFCoreSample> dotnet run Hello World! 
這就開始了,直接使用 VS Code 打開 OracleEFCoreSample 專案目錄,首先要安裝專案需要的 NuGet Packages,可以到 NuGet Gallery  尋找,注意不要裝錯版本。
$ OracleEFCoreSample> dotnet add package Oracle.EntityFrameworkCore --version 2.19.70 $ OracleEFCoreSample> dotnet add package Microsoft.EntityFrameworkCore.Design --version 2.2.6 $ OracleEFCoreSample> dotnet add package Microsoft.EntityFrameworkCore.Relational --version 2.2.6 $ OracleEFCoreSample> dotnet add package Microsoft.Extensions.Configuration.Json --version 3.1.4 
現在專案目錄下的 OracleEFCoreSample.csproj 檔案應該如下:
OracleEFCoreSample.csproj 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <Project  Sdk ="Microsoft.NET.Sdk" >   <PropertyGroup >      <OutputType > Exe</OutputType >      <TargetFramework > netcoreapp3.1</TargetFramework >    </PropertyGroup >    <ItemGroup >      <PackageReference  Include ="Microsoft.EntityFrameworkCore.Design"  Version ="2.2.6"  />      <PackageReference  Include ="Microsoft.EntityFrameworkCore.Relational"  Version ="2.2.6"  />      <PackageReference  Include ="Microsoft.Extensions.Configuration.Json"  Version ="3.1.4"  />      <PackageReference  Include ="Oracle.EntityFrameworkCore"  Version ="2.19.70"  />    </ItemGroup >  </Project > 
一切就緒,首先在專案目錄下開一個 JSON 檔案 appsettings.json, 將專案程式的設定放在此,目前要放的是 Oracle 資料庫的 ConnectionString。
appsettings.json 1 2 3 4 5 6 7 {   "Data" : {     "DefaultConnection" : {       "ConnectionString" : "User Id=xxxx;Password=xxxxxxxx;Data Source=10.11.xx.xxx:1522/xxxx.xxx.com.tw;Connection Timeout=600;min pool size=0;connection lifetime=18000;PERSIST SECURITY INFO=True;"       }   } } 
創建模型 這裡要定義兩個實體類型 Dept 與 Emp,分別映射到 Oracle 資料庫 DEPT 與 EMP 資料表。
在專案目錄下建立一個子目錄 Models 來擺放 Dept.cs 與 Emp.cs。
Models/Dept.cs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 using  System;using  System.Collections.Generic;namespace  OracleEFCoreSample.Models {   public  partial  class  Dept    {     public  Dept (       Emp = new  HashSet<Emp>();     }     public  int  Deptno { get ; set ; }     public  string  Dname { get ; set ; }     public  string  Loc { get ; set ; }     public  virtual  ICollection<Emp> Emp { get ; set ; }   } } 
在 Oracle 資料庫中,DEPT 與 EMP 有 FOREIGN KEY Constraint 的關係,在這個類型中也可以表現出這種 Master-Detail 關係,如果無此需求,可以省略。其實我們只需要第 13、14 與 15 行三個 Deptno、Dname 與 Loc 屬性就可以。
這裡這個 Dept 類型除了三個基本屬性外,Emp 可以把屬於這個部門的員工放進來。
Models/Emp.cs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 using  System;using  System.Collections.Generic;namespace  OracleEFCoreSample.Models {   public  partial  class  Emp    {     public  Emp (       InverseMgrNavigation = new  HashSet<Emp>();     }     public  int  Empno { get ; set ; }     public  string  Ename { get ; set ; }     public  string  Job { get ; set ; }     public  int ? Mgr { get ; set ; }     public  DateTime? Hiredate { get ; set ; }     public  double ? Sal { get ; set ; }     public  double ? Comm { get ; set ; }     public  int ? Deptno { get ; set ; }     public  virtual  Dept DeptnoNavigation { get ; set ; }     public  virtual  Emp MgrNavigation { get ; set ; }     public  virtual  ICollection<Emp> InverseMgrNavigation { get ; set ; }   } } 
EMP 資料表有兩個 FOREIGN KEY Constraint,一個是 Deptno 參照到 DEPT 的 Deptno,另外一個是 Mgr 參照到自己的 Empno。一樣你如果不需要這些關聯,可以只要 13 ~ 20 行的 8 個屬性就可以。型別後面的 ? 號,表示這是個 Nullable value types。 
除了 8 個基本屬性外, DeptnoNavigation 可以把部門資料放進來,MgrNavigation 可以把主管資料放進來,而如果是主管,可以把部屬資料放入 InverseMgrNavigation。 
這兩個型別只是單純的 C# 類別型態定義,與 Oracle 資料庫資料表還看不出映射關係,EF Code 使用了三個概念來定義模型 : 約定、注釋和流利 API(Fluent API)。
按照約定,有些事情會自動發生。例如,用 Id 前綴命名 int 或 Guid 類型的屬性,會將該屬性映射到資料庫的主鍵。例如 EmpId 屬性會自動映射到資料表的主鍵 EmpId。但我們的 Oracle 資料表主鍵通常不會遵守這種規則,所以這裡要用另外一種映射到資料表的約定 : 使用上下文的屬性名 (Fluent API)。
創建上下文 (Context) 這裡要創建另一個 DemoContext 類,透過這個類實現了與 Oracle 資料庫表 DEPT 與 EMP 的關係。這個類派生自基類 DbContext。DemoContext 類定義了 DbSet 與 DbSet 的 Dept 與 Emp 屬性。  
透過 DbContext 派生類的 OnModelCreating 方法使用流利 API。Entity 方法返回一個 EntityTypeBuilder,允許自定義實體,把它映射到特定的資料庫表名(Table)、定義鍵(Key)和索引(Indexes)。
EntityTypeBuilder 定義了一個 Property 方法來配置屬性。Property 方法返回一個 PropertyBuilder,它允許用最大長度、需要的設置和 SQL 類型配置屬性。
要定義一對多映射,EntityTypeBuilder 定義了映射方法。方法 HasMany 與 WithOne 結合,HasMany 需要與 WithOne 鏈接起來。
方法 HasOne 需要和 WithMany 或 WithOne 鏈接起來。鏈接 HasOne 與 WithMany,會定義一對多關聯;鏈接 HasOne 與 WithOne,定義一對一關係。 
Models/DemoContext.cs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 using  System;using  System.IO;using  Microsoft.EntityFrameworkCore;using  Microsoft.EntityFrameworkCore.Metadata;using  Microsoft.Extensions.Configuration;using  Microsoft.Extensions.Configuration.Json;namespace  OracleEFCoreSample.Models {   public  partial  class  DemoContext  : DbContext    {     public  virtual  DbSet<Dept> Dept { get ; set ; }     public  virtual  DbSet<Emp> Emp { get ; set ; }     protected  override  void  OnConfiguring (DbContextOptionsBuilder optionsBuilder )       if  (!optionsBuilder.IsConfigured)       {          optionsBuilder.UseOracle(GetConnectionString(), options =>            options.UseOracleSQLCompatibility("11" )         );       }     }     protected  override  void  OnModelCreating (ModelBuilder modelBuilder )       modelBuilder.HasAnnotation("ProductVersion" , "2.2.6-servicing-10079" )           .HasAnnotation("Relational:DefaultSchema" , "DEMO" );       modelBuilder.Entity<Dept>(entity =>       {         entity.HasKey(e => e.Deptno)                   .HasName("SYS_C0036617" );         entity.ToTable("DEPT" );         entity.HasIndex(e => e.Deptno)                   .HasName("SYS_C0036617" )                   .IsUnique();         entity.Property(e => e.Deptno).HasColumnName("DEPTNO" );         entity.Property(e => e.Dname)                   .HasColumnName("DNAME" )                   .HasColumnType("VARCHAR2" )                   .HasMaxLength(14 );         entity.Property(e => e.Loc)                   .HasColumnName("LOC" )                   .HasColumnType("VARCHAR2" )                   .HasMaxLength(13 );       });       modelBuilder.Entity<Emp>(entity =>       {         entity.HasKey(e => e.Empno)                   .HasName("EMP_PK" );         entity.ToTable("EMP" );         entity.HasIndex(e => e.Empno)                   .HasName("EMP_PK" )                   .IsUnique();         entity.HasIndex(e => e.Ename)                   .HasName("EMP_IDX1" );         entity.Property(e => e.Empno).HasColumnName("EMPNO" );         entity.Property(e => e.Comm)                   .HasColumnName("COMM" )                   .HasColumnType("FLOAT" );         entity.Property(e => e.Deptno)                   .HasColumnName("DEPTNO" );         entity.Property(e => e.Ename)                   .HasColumnName("ENAME" )                   .HasColumnType("VARCHAR2" )                   .HasMaxLength(10 );         entity.Property(e => e.Hiredate)                   .HasColumnName("HIREDATE" )                   .HasColumnType("DATE" );         entity.Property(e => e.Job)                   .HasColumnName("JOB" )                   .HasColumnType("VARCHAR2" )                   .HasMaxLength(9 );         entity.Property(e => e.Mgr).HasColumnName("MGR" );         entity.Property(e => e.Sal)                   .HasColumnName("SAL" )                   .HasColumnType("FLOAT" );         entity.HasOne(d => d.DeptnoNavigation)                   .WithMany(p => p.Emp)                   .HasForeignKey(d => d.Deptno)                   .HasConstraintName("SYS_C0036621" );         entity.HasOne(d => d.MgrNavigation)                   .WithMany(p => p.InverseMgrNavigation)                   .HasForeignKey(d => d.Mgr)                   .HasConstraintName("SYS_C0036620" );       });     }     private  static  string  GetConnectionString (       var  configurationBuilder = new  ConfigurationBuilder()         .SetBasePath(Directory.GetCurrentDirectory())         .AddJsonFile("appsettings.json" );       IConfiguration config = configurationBuilder.Build();       string  connectionString = config["Data:DefaultConnection:ConnectionString" ];       return  connectionString;     }   } } 
DemoContext 是資料庫映射的關鍵,聰明的你就對照著資料庫慢慢斟酌,GetConnectionString 方法則會從 appsettings.json 抓取 Oracle 資料庫的 ConnectionString。 這會用在第 19 行。
同時要注意第 20 行,因為我們的資料庫大部分還是 11g,所以要在 options 中加上 UseOracleSQLCompatibility(“11”),否則會有很多地方會掛掉。 建議趕快升級到 12c(以上)。
讀取資料庫 可以讀取資料庫了。
Program.cs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 using  System;using  System.Linq;using  OracleEFCoreSample.Models;namespace  OracleEFCoreSample {   class  Program    {     static  void  Main (string [] args       Console.WriteLine("Hello Tainan! Hi 台南" );       using  (var  db = new  DemoContext())       {         Console.WriteLine("===== Departments ==========" );         foreach  (var  dept in  db.Dept)         {           Console.WriteLine($"{dept.Deptno}  {dept.Dname}  {dept.Loc} " );         }         Console.WriteLine("===== Employees ==========" );         foreach  (var  emp in  db.Emp)         {           Console.WriteLine($"{emp.Empno}  {emp.Ename}  {emp.Job}  {emp.Mgr}  {emp.Hiredate?.ToString("yyyy-MM-ddTHH:mm:ss" )}  {emp.Sal}  {emp.Comm}  {emp.Deptno} " );         }       }     }   } } 
$ OracleEFCoreSample> dotnet run Hello Tainan! Hi 台南 ===== Departments ========== 10 會計部 紐約 20 RESEARCH DALLAS 30 SALES 台南 40 OPERATIONS BOSTON 70 資訊部 台南 B4 60 開發部 台南 ===== Employees ========== 7839 KING PRESIDENT  1981-11-17T00:00:00 5000  10 7698 BLAKE MANAGER1 7839 1981-05-01T00:00:00 2850 101 30 7782 陳瑞 MANAGER 7902 1981-06-09T00:00:00 2400  10 7566 陳賜珉 MANAGER 7839 1981-04-02T00:00:00 2975  20 7788 SCOTT ANALYST 7566 1982-12-09T00:00:00 45300  20 7902 FORD ANALYST 7566 1981-12-03T00:00:00 3000  20 7369 SMITH CLERK 7902 1980-12-17T00:00:00 8001  20 7499 ALLEN SALESMAN 7698 1981-02-20T00:00:00 1600 303 30 7608 馬小九 ANALYST 7788 2010-06-28T00:00:00 1000 100 40 7654 葉習? SALESMAN 7698 1981-09-28T00:00:00 1250 1400 30 7844 ??? SALESMAN 7698 1981-09-08T00:00:00 1500  30 7876 ADAMS CLERK 7788 1983-01-12T00:00:00 1100  20 7900 JAMES CLERK 7698 1981-12-03T00:00:00 94998  30 7934 楊? CLERK 7902 1982-01-23T00:00:00 1500  10 9006 李逸君 ANALYST 7788 2001-05-07T00:00:00 66666  70 7607 ??? 分析師 7788 2008-03-24T00:00:00 45000 100 70 7609 蔡大一 分析師 7788 2010-06-28T00:00:00 60000  70 9011 文英蔡 總鋪師 7788 2018-08-28T00:00:00 77778 180 40 8907 牸? ANALYST 7566 1982-12-09T00:00:00 9002  10 
模型建立好,讀取資料就這麼簡單,加入 LINQ 看看。
Program.cs 1 2 3 4 5 6 7 8 9 10 11 12 using  (var  db = new  DemoContext()){   var  employees = db.Emp     .Where(e => e.Deptno == 10  || e.Deptno == 30 )     .OrderBy(e => e.Deptno)     .ThenBy(e => e.Empno);   foreach  (var  e in  employees)   {     Console.WriteLine($"{e.Empno}  {e.Ename}  {e.Job}  {e.Mgr}  {e.Hiredate?.ToString("yyyy-MM-ddTHH:mm:ss" )}  {e.Sal}  {e.Comm}  {e.Deptno} " );   } } 
現在將 Master-Detail 資料關聯起來,DEPT 對 EMP 是一對多的關係。
Program.cs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 using  (var  db = new  DemoContext()){   var  employees = db.Emp.ToList();   foreach  (var  dept in  db.Dept)   {     dept.Emp = employees.Where(d => d.Deptno == dept.Deptno).ToList();   }   var  department = db.Dept.Where(d => d.Deptno == 20 ).FirstOrDefault();   Console.WriteLine($"=== {department.Deptno}  {department.Dname}  {department.Loc}  ===" );   foreach  (var  e in  department.Emp)   {     Console.WriteLine($"{e.Empno}  {e.Ename}  {e.Job}  {e.Mgr}  {e.Hiredate?.ToString("yyyy-MM-ddTHH:mm:ss" )}  {e.Sal}  {e.Comm}  {e.Deptno} " );   } } 
$ OracleEFCoreSample> dotnet run Hello Tainan! Hi 台南 === 20 RESEARCH DALLAS === 7566 陳賜珉 MANAGER 7839 1981-04-02T00:00:00 2975  20 7788 SCOTT ANALYST 7566 1982-12-09T00:00:00 45300  20 7902 FORD ANALYST 7566 1981-12-03T00:00:00 3000  20 7369 SMITH CLERK 7902 1980-12-17T00:00:00 8001  20 7876 ADAMS CLERK 7788 1983-01-12T00:00:00 1100  20 
主管與部屬關係。
Program.cs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 using  (var  db = new  DemoContext()){   var  employees = db.Emp.ToList();   var  managers = employees.GroupBy(e => e.Mgr)     .Select(grp => grp.FirstOrDefault())     .Select(e => new  { Manager = e.Mgr ?? e.Empno });   Emp manager = null ;   foreach  (var  m in  managers)   {     manager = employees.Where(e => e.Empno == m.Manager).FirstOrDefault();     manager.InverseMgrNavigation = employees.Where(e => e.Mgr == manager.Empno).ToList();   }   manager = employees.Where(e => e.Empno == 7788 ).FirstOrDefault();   Console.WriteLine($"=== {manager.Empno}  {manager.Ename}  ===" );   foreach  (var  e in  manager.InverseMgrNavigation)   {     Console.WriteLine($"{e.Empno}  {e.Ename}  {e.Job}  {e.Mgr}  {e.Hiredate?.ToString("yyyy-MM-ddTHH:mm:ss" )}  {e.Sal}  {e.Comm}  {e.Deptno} " );   } } 
$ OracleEFCoreSample> dotnet run Hello Tainan! Hi 台南 === 7788 SCOTT === 7608 馬小九 ANALYST 7788 2010-06-28T00:00:00 1000 100 40 7876 ADAMS CLERK 7788 1983-01-12T00:00:00 1100  20 9006 李逸君 ANALYST 7788 2001-05-07T00:00:00 66666  70 7607 ??? 分析師 7788 2008-03-24T00:00:00 45000 100 70 7609 蔡大一 分析師 7788 2010-06-28T00:00:00 60000  70 9011 文英蔡 總鋪師 7788 2018-08-28T00:00:00 77778 180 40 
新增紀錄 Add 方法把 Emp 物件添加到 DemoContext 上下文中,還沒有寫入資料庫中,SaveChanges 方法把 Emp 物件寫入資料庫。
Program.cs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 using  (var  db = new  DemoContext()){    db.Emp.Add(new  Emp     {      Empno = 9588 ,      Ename = "C#範例" ,      Job = "ANALYST" ,      Mgr = 7566 ,      Hiredate = DateTime.Now,      Sal = 23000 ,      Comm = 100 ,      Deptno = 40     }   );   int  records = db.SaveChanges();   Console.WriteLine($"{records}  records saved to database." );  } 
還可以用 AddRange 一次寫入多個物件。
Program.cs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 using  (var  db = new  DemoContext()){    db.Emp.AddRange(      new  Emp       {        Empno = 9681 ,        Ename = "C#範例2" ,        Job = "ANALYST" ,        Mgr = 7566 ,        Hiredate = DateTime.Now,        Sal = 13000 ,        Comm = 130 ,        Deptno = 10       },      new  Emp      {        Empno = 9682 ,        Ename = "C#範例3" ,        Job = "ANALYST" ,        Mgr = 7566 ,        Hiredate = DateTime.Now,        Sal = 33000 ,        Comm = 330 ,        Deptno = 20       }   );   int  records = db.SaveChanges();   Console.WriteLine($"{records}  records saved to database." );  } 
更新紀錄 Program.cs 1 2 3 4 5 6 7 8 9 10 11 using  (var  db = new  DemoContext()){   int  records = 0 ;     var  employee = db.Emp.Where(e => e.Empno == 9588 ).FirstOrDefault();   if  (employee != null )   {     employee.Comm = 500 ;       records = db.SaveChanges();   }    Console.WriteLine($"{records}  records updated." );  } 
刪除紀錄 Program.cs 1 2 3 4 5 6 7 8 9 10 11 using  (var  db = new  DemoContext()){   int  records = 0 ;     var  employee = db.Emp.Where(e => e.Empno == 9588 ).FirstOrDefault();   if  (employee != null )   {     db.Emp.Remove(employee);     records = db.SaveChanges();   }    Console.WriteLine($"{records}  records deleted from database." );  } 
可以用 RemoveRange 刪除多筆資料。
Program.cs 1 2 3 4 5 6 7 using  (var  db = new  DemoContext()){   var  employees = db.Emp.Where(e => e.Empno == 9681  || e.Empno == 9682 );   db.Emp.RemoveRange(employees);   int  records = db.SaveChanges();          Console.WriteLine($"{records}  records deleted from database." );  } 
物件關係映射工具,如 EF Core,並不適用於所有場景。例如一次刪除很多資料或所有的資料就沒有那麼高效能,使用單個 SQL 語句可以刪除所有資料,會比每一個紀錄使用一個 DELETE 語句效能高得多。 
反向工程 (Reverse Engineering) 在反向工程之前,您必須安裝 CLI  工具。
$ > dotnet tool install --global dotnet-ef 
然後在你的專案目錄下執行反向工程,下面會產生 DEPT 與 EMP 的反向工程。 它會在專案目錄下產生子目錄 Models 與 Dept.cs、Emp.cs 與 ModelContext.cs。 先前例子的 Dept.cs、Emp.cs 與 DemoContext.cs 有經過修正,可以對照看看,尤其是 Emp.Sal 屬性的型別。 
$ OracleEFCoreSample> dotnet ef dbcontext scaffold "User Id=xxxx;Password=xxxxxxxx;Data Source=10.11.xx.xxx:1522/xxxx.xxx.com.tw;Connection Timeout=600;min pool size=0;connection lifetime=18000;PERSIST SECURITY INFO=True;"  Oracle.EntityFrameworkCore --table EMP --table DEPT -o Models -f 
下面則是 Schema 的所有資料表。
$ OracleEFCoreSample> dotnet ef dbcontext scaffold "User Id=xxxx;Password=xxxxxxxx;Data Source=10.11.xx.xxx:1522/xxxx.xxx.com.tw;Connection Timeout=600;min pool size=0;connection lifetime=18000;PERSIST SECURITY INFO=True;"  Oracle.EntityFrameworkCore -o Models -f