大夥伴們只要學過三天 EF Core 一定知道,.NET SDK 有一個 dotnet-ef 工具(需要安裝),可以用來創建/遷移數據庫、生成模型代碼、優化模型和查詢代碼等。必要時還能生一個單獨的 exe,可以運行它來更新數據庫結構。
不過,按照官方的設計思路,肯定不會把所有功能都堆在 exe 項目中的,這不,dotnet-ef 只是做個封裝,可以通過命令行執行罷了,其實核心功能是寫在 Design 包裏面(Nuget 包名:Microsoft.EntityFrameworkCore.Design)。於是,咱們可以開發自己的 EF 輔助工具。比如,你可以把命令行操作的功能搞成窗口圖形化操作。當然,這些功能僅限開發者使用,用户一般不需要(不一般的用户除外)。
如此,在 DB First 方案(先有數據庫)中,咱們可以把生成實體類以及 dbContext 類的功能直接寫到項目代碼中,然後加上一個條件編譯,在需要生成代碼時開啓一下編譯符號,運行一個項目就能生成實體模型了。其他情況下把條件編譯符號註釋掉就可以。
這個功能就有點像 Sugar 的玩法。老周在某些項目中就是這麼幹的。不過老周更喜歡 EF,理由有:
1、EF Core 更靈活。
2、EF Core 的表達樹翻譯功能比 Sugar 完善,功能更多。
3、有官方支持的優先用原則,沒有才考慮第三方。
好了,不扯廢話了,咱們開始!
一、基礎知識
首先,咱們要明確功能:數據庫已經有了,可能是你創建的,可能是別人創建的。很多團隊都會把搞數據庫,寫存儲過程的單獨一堆人去幹,然後,項目的非數據庫部分另一堆人去做。所以,小型數據庫才考慮用 EF Core 去創建,複雜的數據庫還是先創建數據庫好一些。咱們要做的就是根據現有的數據庫和表,直接生成實體類和 DbContext 的派生類。
在分析思路之前,既然大夥兒都是玩 .NET 的,那就堅守這個原則:處處都是服務容器和依賴注入。
好,有這個思想準備,咱們才能講知識點。咱們來認識幾個新朋友,熟悉一下,以後才能好好利用他們,嗯,朋友是拿來利用的。
第一位,本名 IDatabaseModelFactory。他的絕活本領是爆庫。你要從數據庫生成實體,那你得知道數據庫裏有哪些表,表中有哪些列,哪個是主鍵,列的類型是什麼……沒事,這些信息交給這位朋友就行。
調用以下方法,你能得到一個 DatabaseModel 對象。
DatabaseModel Create(string connectionString, DatabaseModelFactoryOptions options);
第一個參數就是連接字符串,這個不用介紹了吧。要爆庫你得知道庫在哪裏吧。第二個參數是選項類,用來配置相關參數的。
public DatabaseModelFactoryOptions(IEnumerable<string>? tables = null, IEnumerable<string>? schemas = null) { Tables = tables ?? []; Schemas = schemas ?? []; }
上述是它的構造函數,Tables 是你要告訴朋友,你想要哪些表;Schemas 表示你要的架構,比如 dbo。這兩個參數都可以是 null,如果是 null 表示要全庫爆。
Create 方法返回的 DatabaseModel 對象包含數據庫名、數據庫中表、列、主鍵、外鍵、索引等相關信息。
好了,拿到數據庫信息了,輪到第二位朋友出場—— IScaffoldingModelFactory。他的絕活是加工,把你從數據庫中爆出來的信息處理後,直接返回一個 IModel。對,就是 EF Core 中用的數據庫模型,所以,如果你不打算生成代碼,這時候你完全可以把這個 IModel 作為 DbContext 的外部模型使用。還記得老周寫過使用外部模型的水文嗎?在 DbContext 選項配置時,用 UseModel 方法。
但,建議你不要這麼幹,你想想每次運行程序都要爆一次數據庫再轉換為 EF Core 模型,既浪費性能也沒啥實際意義。所以,這個生成的 IModel 還要進一步處理。
第三位朋友叫 IModelCodeGeneratorSelector。他是一名零件選配師,他會根據你的需要幫你找到合適的專屬文員(代碼生成器)。這位朋友有一個 Select 方法,調用後進行篩選。
[Obsolete("Use the overload that takes ModelCodeGenerationOptions instead.")] IModelCodeGenerator Select(string? language); // 注意,上面的方法過時,而下面的方法又反過去調用它 IModelCodeGenerator Select(ModelCodeGenerationOptions options) #pragma warning disable CS0618 // Type or member is obsolete => Select(options.Language); #pragma warning restore CS0618
Select 的舊版本已被標記為過時,但下面的方法又調用它。這是什麼騷操作?這是官方團隊的兼容操作。過時的方法只有一個字符串參數,表示生成的代碼語言(“VB”,“C#”)。而新方法的參數是一個 ModelCodeGenerationOptions 選項類,可以配置更多東西。
Select 方法幫你選好了心儀的文員妹妹,她叫 IModelCodeGenerator。她雖然學歷不高,但很勤奮很務實,你可以相信她。調用她的 GenerateModel 方法,她會幫你生成代碼。
ScaffoldedModel GenerateModel(
IModel model,
ModelCodeGenerationOptions options);
model 參數就是第二位朋友 IScaffoldingModelFactory 幫你生成的模型;options 參數是選項,和 IModelCodeGeneratorSelector.Select 方法用的是同一個。
到這裏基本工作就完成了,返回的 ScaffoldedModel 對象中已經包含代碼,以及代碼要存放的路徑了。不過,這些目前還在內存中,未真正寫入磁盤文件。程序退出後就沒了。
public class ScaffoldedModel { // dbContext 類的代碼,以及文件路徑 public virtual ScaffoldedFile ContextFile { get; set; } = null!; // 附加文件,通常是實體類的代碼以及文件路徑,每個實體類佔一個文件 public virtual IList<ScaffoldedFile> AdditionalFiles { get; } = new List<ScaffoldedFile>(); } public class ScaffoldedFile(string path, string code) { // 代碼要存入的文件路徑 public virtual string Path { get; set; } = path; // 已生成的代碼 public virtual string Code { get; set; } = code; }
總結一下,流程如下:
A、獲取數據庫信息;
B、生成設計時模型;
C、生成代碼;
D、保存代碼。
你一定會抱怨了,這過程有點複雜。別急,還沒完呢,繼續往下看,簡單的來了。
上面提到的幾位朋友,你一個個地告訴他們幹什麼是有些麻煩的,所以,把他們組成一個團隊,設立一名管理者,有事只要跟他們的老大説行了。這位由不民主制度任命的老大叫 IReverseEngineerScaffolder(位於 Microsoft.EntityFrameworkCore.Scaffolding 命名空間),默認實現類是 ReverseEngineerScaffolder(位於 Microsoft.EntityFrameworkCore.Scaffolding.Internal 命名空間)。雖然這個類是 public 的,但官方團隊讓它藏在 Internal 命名空間下,這表明:在功能上是不希望外部代碼訪問的。在使用時,咱們的確不用訪問該類,而是通過 IReverseEngineerScaffolder 接口來調用。
前面介紹的幾位朋友,吃過幾回飯後你可以忘記,現在你只要記住 IReverseEngineerScaffolder 即可。有事找他。
IReverseEngineerScaffolder 接口定義了兩個方法:
ScaffoldedModel ScaffoldModel( string connectionString, DatabaseModelFactoryOptions databaseOptions, ModelReverseEngineerOptions modelOptions, ModelCodeGenerationOptions codeOptions); SavedModelFiles Save( ScaffoldedModel scaffoldedModel, string outputDir, bool overwriteFiles);
ScaffoldModel 方法根據數據庫生成代碼,Save 方法把代碼寫入文件。這樣一來,是不是變得簡單了?只要一個服務接口,調用兩個方法成員就完事了。
二、如何使用
既然處處是注入,那就得先初始化服務集合。Design 庫提供了兩個擴展方法:
public static IServiceCollection AddDbContextDesignTimeServices( this IServiceCollection services, DbContext context); public static IServiceCollection AddEntityFrameworkDesignTimeServices( this IServiceCollection services, IOperationReporter? reporter = null, Func<IServiceProvider>? applicationServiceProviderAccessor = null);
第一個擴展方法在生成實體代碼時不需要,它的作用是把現有 DbContext 實例中的服務添加到服務容器中。咱們今天要實現的功能要用到第二個方法,它會添加設計時的一些基礎服務,包括我們上面提到的幾位新朋友。
當然,這是設計時的基礎服務,不包括 EF 核心服務。核心服務一般可以通過實現 IDesignTimeServices 接口來添加。
public interface IDesignTimeServices { void ConfigureDesignTimeServices(IServiceCollection serviceCollection); }
實現接口時,在 ConfigureDesignTimeServices 方法中,向服務容器添加需要的服務。
然後,在程序集級別使用 [DesignTimeProviderServices] 特性指定你實現 IDesignTimeServices 接口的類的完整名稱(連同命名空間)。其他工具(如 dotnet-ef,或你自己實現的工具)可以通過反射獲取它,動態實例化並調用 ConfigureDesignTimeServices 方法。當然,你不用反射,直接在代碼中 new 也可以的。
其實,你完全可以偷懶,不用去實現 IDesignTimeServices 接口,因為每種數據庫的提供者都會實現專用的類。比如 SQL Server 的提供者,會有一個 SqlServerDesignTimeServices 類,並且應用 [DesignTimeProviderServices] 特性。
[assembly: DesignTimeProviderServices("Microsoft.EntityFrameworkCore.SqlServer.Design.Internal.SqlServerDesignTimeServices")]
對於 SQLite 數據庫,會有一個 SqliteDesignTimeServices 類,同樣也會在程序集上應用 [DesignTimeProviderServices] 特性。
[assembly: DesignTimeProviderServices("Microsoft.EntityFrameworkCore.Sqlite.Design.Internal.SqliteDesignTimeServices")]
這些默認實現的設計時服務提供類默認會把 EF 的核心服務、關係數據庫相關、數據庫專用的服務全部添加到服務容器,你不需要額外去處理。
當所有需要的服務都添加到容器後,生成 ServiceProvider。然後你直接從服務容器中獲取 IReverseEngineerScaffolder 接口,配置好相關參數(如輸出目錄、DbContext 類的名稱等),先調用 ScaffoldModel 方法生成代碼,再調用 Save 方法寫入文件就行了。
三、實例演示
光説不練,慘過失戀。前文已介紹完所有基礎知識了,該練練手了。
老周以 SQL Server 來演示,先創建數據庫,以及兩張表。一張表是客户表,一張是照片表。因為這是一家照相館的信息管理系統。一位客户可以有多張照片,所以是“一對多”的關係(別問我怎麼沒有多對多,你一張照片給多個客户?這麼有分享精神的嗎?除非是大合照)。
USE [master] GO CREATE DATABASE [SomeDB] GO CREATE TABLE [dbo].[tb_customers] ( [cust_id] INT IDENTITY (1, 1) NOT NULL, [name] NVARCHAR (12) NOT NULL, [age] INT NULL, [address] NVARCHAR (100) NULL, [phone] CHAR (11) NOT NULL, [email] NVARCHAR (64) NULL, [remark] NTEXT NULL, CONSTRAINT [PK_customers] PRIMARY KEY CLUSTERED ([cust_id] ASC) ); CREATE TABLE [dbo].[tb_photos] ( [Id] INT IDENTITY (1, 1) NOT NULL, [tags] NVARCHAR (30) NOT NULL, [dpi] REAL DEFAULT ((300.0)) NULL, [width] FLOAT (53) NOT NULL, [height] FLOAT (53) NOT NULL, [cust_id] INT DEFAULT ((0)) NOT NULL, CONSTRAINT [PK_photos] PRIMARY KEY CLUSTERED ([Id] ASC), CONSTRAINT [FK_photos_custs] FOREIGN KEY ([cust_id]) REFERENCES [dbo].[tb_customers] ([cust_id]) ON DELETE CASCADE ON UPDATE CASCADE );
注意外鍵是在 tb_photos 表中定義的,這裏外鍵不唯一,要是搞唯一了就變成“一對一”關係了。
創建一個最簡單的.NET項目(控制枱),你需要向項目添加以下 nuget 包:
<ItemGroup> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.0"> <PrivateAssets>all</PrivateAssets> <!--<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>--> </PackageReference> <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.0" /> </ItemGroup>
嚴重注意:Design 是開發工具包,默認是不讓你的代碼訪問的。當然,如果你考慮用反射的方法調用,那無所謂。這裏咱們沒必要去反射,應該直接訪問,所以,在 PackageReference 元素下,把 IncludeAssets 整個節點註釋掉,這樣就能直接訪問了。
其他的包都常規操作了,老周這裏用的是 SQL Server就用 Sqlserver 包,你用的如果是 SQLite 那就 Sqlite 包。這個就不多説了,都懂的。
咱們並不是每次運行程序都需要生成代碼的,除非是有改動(通常不會改,小改動的話也不用重新生成,直接手動改代碼就行),所以定義一個符號,當需要生成實體代碼時啓用,畢竟這是為開發者服務的功能,不是面向最終用户。
#define GEN_CODES …… static void Main(string[] args) { #if GEN_CODES GenModelCodes(); #endif }
下面我們把注意力集中到 GenModelCodes 方法上。
#if GEN_CODES private static void GenModelCodes() { …… }
#endif
用常量定義一些基本參數。
// 連接字符串 const string connectStr = "Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=SomeDB"; // 輸出目錄 const string outputDir = "..\\..\\..\\DBModels"; // 命名空間 const string myNamespace = "DB"; // 數據庫上下文名稱 const string contextName = "SomeDbContext";
outputDir 常量指定的是輸出目錄,可以用相對路徑(相對於當前程序),老周這裏用了三個 ..,即往上跳三層目錄。你猜猜是啥目錄?(項目根目錄)
對於命名空間,可以有兩個,一個是 DbContext 派生類所在命名空間,一個是實體類所在命名空間。當然,老周這裏只用了一個 DB,意思就是它們都位於 DB 命名空間內。
contextName 常量指示生成的 DbContext 子類的名字,這裏老周給了它一個風雅的名字 SomeDbContext。
接着,咱們需要配置三個選項類(上文介紹過了,雖然名字有點臭長,但不用死記,大概記得就行)。
DatabaseModelFactoryOptions dmfacOpts = new( // 選擇你要的表 tables: ["tb_customers", "tb_photos"], // 選擇你要的架構 schemas: ["dbo"] ); ModelCodeGenerationOptions modgenOpt = new() { Language = "C#", // 語言可以不設置,默認 C# ContextDir = "", ModelNamespace = myNamespace, ContextNamespace = myNamespace, ContextName = contextName, UseDataAnnotations = false, UseNullableReferenceTypes = true }; ModelReverseEngineerOptions modreverOpt = new() { UseDatabaseNames = true, NoPluralize = false };
1、DatabaseModelFactoryOptions 選項:配置一下我們需要用的表和架構,其實這裏可以全 null,畢竟咱們全部生成。
2、ModelCodeGenerationOptions 選項:生成代碼相關。
- Language 屬性可以忽略的,默認就是 C#;
- ContextDir 屬性可以為 dbContext 類指定一個子目錄(相對於 outputDir),空字符串表示只放在 outputDir 下,不用單獨子目錄;
- ModelNamespace 屬性指定實體類代碼的命名空間;
- ContextNamespace 屬性指定 dbContext 類的命名空間。可以與實體類在同一命名空間;
- ContextName 屬性指定 dbcontext 類的類名;
- UseDataAnnotations 屬性表示用不用數據批註來配置模型,false 表示用 ModelBuilder 來配置,重寫 DbContext.OnModelCreating 方法;
- UseNullableReferenceTypes 屬性配置用不用可以為 null 類型,比如 string?、int?;
- ConnectionString 屬性是連接字符串,不用配置,因為 IReverseEngineerScaffolder.ScaffoldModel 方法的第一個參數就是連接字符串;
- ProjectDir 屬性是 .NET 項目所在目錄,這裏不用配置,因為連 DbContext 都沒有,這裏用不上。
3、ModelReverseEngineerOptions 選項:UseDatabaseNames 表示是否用數據庫原有的名字,即實體屬性等命名與數據庫中一樣;如果不用,那麼會生成 C# 命名風格的名稱,如 Name、TbCustomer 等。NoPluralize 表示禁用複數,這個主要是 dbcontext 類中 DbSet 類型屬性的命名,如 Students、Customers 等,false 表示不禁用。
下一步就是服務容器配置了。
ServiceCollection services = new();
添加設計時基礎服務。
services.AddEntityFrameworkDesignTimeServices();
然後就是獲取 IDesignTimeServices 服務了。有兩種方法:
第一種方法,我們是知道的,面向 SQL Server 的設計時服務類叫 SqlServerDesignTimeServices。對,直接 new 一下就好,最簡單。
IDesignTimeServices designtimeSvc = new SqlServerDesignTimeServices(); designtimeSvc.ConfigureDesignTimeServices(services);
記得調用 ConfigureDesignTimeServices 方法,否則白乾活。SqlServerDesignTimeServices 類雖然聲明上是 public,但功能意義上是內部類型,編譯器會發出 EF1001 警告。在使用類之前的任意位置禁用這個警告。
#pragma warning disable EF1001
第二種方法是運用反射,代碼雖然多一點,但不用禁用 EF1001 警告。
Assembly sqlserverProvdAssm = typeof(SqlServerServiceCollectionExtensions).Assembly; DesignTimeProviderServicesAttribute? attr = sqlserverProvdAssm.GetCustomAttribute<DesignTimeProviderServicesAttribute>(); if (attr == null) { Console.WriteLine("這個程序集不對勁!"); return; } Type? designSvcType = sqlserverProvdAssm.GetType(attr.TypeName); if (designSvcType == null) { Console.WriteLine("鬧鬼了,居然找不到類型"); return; } // 創建實例 IDesignTimeServices designtimeSvc = (IDesignTimeServices)Activator.CreateInstance(designSvcType)!; // 調用方法配置服務 designtimeSvc.ConfigureDesignTimeServices(services);
由於我們項目已經引用了 Microsoft.EntityFrameworkCore.SqlServer 包,所以不需要再 Load 程序集了,它已經 Load 了。所以你在這個程序集中隨便選個公共類,獲取其 Type,就能得到 Assembly 了。我選的是 SqlServerServiceCollectionExtensions 類,是個定義擴展方法的類。
獲取到程序集後,拿到 DesignTimeProviderServices 特性。
DesignTimeProviderServicesAttribute? attr = sqlserverProvdAssm.GetCustomAttribute<DesignTimeProviderServicesAttribute>();
這個特性實例的 TypeName 屬性就是 SqlServerDesignTimeServices 類的全名。然後用 Activator.CreateInstance 方法動態創建其實例,賦值給 IDesignTimeServices 接口類型的變量,最後調用 ConfigureDesignTimeServices 方法就行了。
配置完服務容器後,生成一下服務 Provider。
IServiceProvider serviceProvider = services.BuildServiceProvider();
接下來,見證奇蹟的時候到了。從服務容器中獲取 IReverseEngineerScaffolder。
IReverseEngineerScaffolder scaffolder = serviceProvider.GetRequiredService<IReverseEngineerScaffolder>();
輕鬆地調用 ScaffoldModel 方法。
var scaffModel = scaffolder.ScaffoldModel( connectionString: connectStr, databaseOptions: dmfacOpts, modelOptions: modreverOpt, codeOptions: modgenOpt );
代碼已經生成,要保存到文件。
if (scaffModel != null) { var res = scaffolder.Save( scaffoldedModel: scaffModel, outputDir: outputDir, overwriteFiles: true // 覆蓋文件 ); if (res is not null) { Console.WriteLine("dbContext路徑:{0}", res.ContextFile); Console.WriteLine("實體路徑:"); foreach (string f in res.AdditionalFiles) { Console.WriteLine(" {0}", f); } } }
整個 GenModelCodes 方法的代碼如下:
#if GEN_CODES private static void GenModelCodes() { // 連接字符串 const string connectStr = "Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=SomeDB"; // 輸出目錄 const string outputDir = "..\\..\\..\\DBModels"; // 命名空間 const string myNamespace = "DB"; // 數據庫上下文名稱 const string contextName = "SomeDbContext"; // 準備選項 DatabaseModelFactoryOptions dmfacOpts = new( // 選擇你要的表 tables: ["tb_customers", "tb_photos"], // 選擇你要的架構 schemas: ["dbo"] ); ModelCodeGenerationOptions modgenOpt = new() { Language = "C#", // 語言可以不設置,默認 C# ContextDir = "", ModelNamespace = myNamespace, ContextNamespace = myNamespace, ContextName = contextName, UseDataAnnotations = false, UseNullableReferenceTypes = true }; ModelReverseEngineerOptions modreverOpt = new() { UseDatabaseNames = true, NoPluralize = false }; // 服務集合 ServiceCollection services = new(); // 1、設計時基礎服務 services.AddEntityFrameworkDesignTimeServices(); // 2、數據庫提供的設計時服務,它已包含框架基礎服務 // 直接實例化 IDesignTimeServices designtimeSvc = new SqlServerDesignTimeServices(); designtimeSvc.ConfigureDesignTimeServices(services); // 或使用反射 /* Assembly sqlserverProvdAssm = typeof(SqlServerServiceCollectionExtensions).Assembly; DesignTimeProviderServicesAttribute? attr = sqlserverProvdAssm.GetCustomAttribute<DesignTimeProviderServicesAttribute>(); if (attr == null) { Console.WriteLine("這個程序集不對勁!"); return; } Type? designSvcType = sqlserverProvdAssm.GetType(attr.TypeName); if (designSvcType == null) { Console.WriteLine("鬧鬼了,居然找不到類型"); return; } // 創建實例 IDesignTimeServices designtimeSvc = (IDesignTimeServices)Activator.CreateInstance(designSvcType)!; // 調用方法配置服務 designtimeSvc.ConfigureDesignTimeServices(services); */ // 構建服務 IServiceProvider serviceProvider = services.BuildServiceProvider(); // 生成代碼 IReverseEngineerScaffolder scaffolder = serviceProvider.GetRequiredService<IReverseEngineerScaffolder>(); var scaffModel = scaffolder.ScaffoldModel( connectionString: connectStr, databaseOptions: dmfacOpts, modelOptions: modreverOpt, codeOptions: modgenOpt ); // 完事後還得保存 if (scaffModel != null) { var res = scaffolder.Save( scaffoldedModel: scaffModel, outputDir: outputDir, overwriteFiles: true // 覆蓋文件 ); if (res is not null) { Console.WriteLine("dbContext路徑:{0}", res.ContextFile); Console.WriteLine("實體路徑:"); foreach (string f in res.AdditionalFiles) { Console.WriteLine(" {0}", f); } } } } #endif
現在,你可以運行一下試試(連接字符串記得改一下,別照抄)。然後你會得到以下寶藏:
看看生成的代碼。
using System; using System.Collections.Generic; namespace DB; public partial class tb_customer { public int cust_id { get; set; } public string name { get; set; } = null!; public int? age { get; set; } public string? address { get; set; } public string phone { get; set; } = null!; public string? email { get; set; } public string? remark { get; set; } public virtual ICollection<tb_photo> tb_photos { get; set; } = new List<tb_photo>(); } /************************************************************/ using System; using System.Collections.Generic; namespace DB; public partial class tb_photo { public int Id { get; set; } public string tags { get; set; } = null!; public float? dpi { get; set; } public double width { get; set; } public double height { get; set; } public int cust_id { get; set; } public virtual tb_customer cust { get; set; } = null!; }
using System; using System.Collections.Generic; using Microsoft.EntityFrameworkCore; namespace DB; public partial class SomeDbContext : DbContext { public SomeDbContext() { } public SomeDbContext(DbContextOptions<SomeDbContext> options) : base(options) { } public virtual DbSet<tb_customer> tb_customers { get; set; } public virtual DbSet<tb_photo> tb_photos { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) #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 https://go.microsoft.com/fwlink/?LinkId=723263. => optionsBuilder.UseSqlServer("Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=SomeDB"); protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<tb_customer>(entity => { entity.HasKey(e => e.cust_id).HasName("PK_customers"); entity.Property(e => e.address).HasMaxLength(100); entity.Property(e => e.email).HasMaxLength(64); entity.Property(e => e.name).HasMaxLength(12); entity.Property(e => e.phone) .HasMaxLength(11) .IsUnicode(false) .IsFixedLength(); entity.Property(e => e.remark).HasColumnType("ntext"); }); modelBuilder.Entity<tb_photo>(entity => { entity.HasKey(e => e.Id).HasName("PK_photos"); entity.Property(e => e.dpi).HasDefaultValue(300f); entity.Property(e => e.tags).HasMaxLength(30); entity.HasOne(d => d.cust).WithMany(p => p.tb_photos) .HasForeignKey(d => d.cust_id) .HasConstraintName("FK_photos_custs"); }); OnModelCreatingPartial(modelBuilder); } partial void OnModelCreatingPartial(ModelBuilder modelBuilder); }
看看 SomeDbContext 類的 tb_customers 和 tb_photos 屬性,ModelReverseEngineerOptions 選項類中的 NoPluralize 屬性配置的就是這裏(屬性命名使用複數,當然,如果你生成的是中文名,那無所謂)。
重寫 OnConfiguring 方法,配置數據庫連接的地方有個警告。
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) #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 https://go.microsoft.com/fwlink/?LinkId=723263. => optionsBuilder.UseSqlServer("Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=SomeDB");
意思是提醒你不要把連接字符串硬編碼,這個後面咱們可以自己改代碼,使用配置文件中的連接字符串。
好了,今天的話題聊到這兒。下一篇咱們聊 Code First 方案下,用編程方式去生成遷移代碼,並遷移數據庫。