【EF Core】“DB First”方案下用编程方式生成数据库模型代码

大伙伴们只要学过三天 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

复制代码

现在,你可以运行一下试试(连接字符串记得改一下,别照抄)。然后你会得到以下宝藏:

 

image

 

看看生成的代码。

 

复制代码

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");

在 Entity Framework Core 中使用数据库优先(DB First)的方法进行开发,主要是通过已有的数据库结构生成对应的实体类和 `DbContext` 类。以下是详细的操作指南: ### 安装必要的 NuGet 包 在使用 EF Core数据库优先模式之前,需要确保项目中已经安装了以下 NuGet 包: - `Microsoft.EntityFrameworkCore.SqlServer`:提供对 SQL Server 数据库的支持。 - `Microsoft.EntityFrameworkCore.Tools`:用于执行脚手架命令,例如从数据库生成实体类和上下文类。 可以通过 NuGet Package Manager 或者使用 .NET CLI 命令来安装这些包: ```bash dotnet add package Microsoft.EntityFrameworkCore.SqlServer dotnet add package Microsoft.EntityFrameworkCore.Tools ``` ### 使用 Scaffold-DbContext 命令生成实体类与 DbContext 在安装好所需的依赖后,可以使用 `Scaffold-DbContext` 命令从现有数据库生成实体类和 `DbContext`。该命令的基本语法如下: ```powershell Scaffold-DbContext [-Connection] <String> [-Provider] <String> [-OutputDir <String>] [-Context <String>] [-Schemas <String>] [-Tables <String>] [-DataAnnotations] [-Force] [-Project <String>] [-StartupProject <String>] [-Environment <String>] ``` 例如,使用 SQL Server 数据库,连接字符串为 `Server=(localdb)\mssqllocaldb;Database=Company;Trusted_Connection=True;`,并且希望将生成代码输出到 `Models` 文件夹中,可以执行以下命令: ```powershell Scaffold-DbContext "Server=(localdb)\mssqllocaldb;Database=Company;Trusted_Connection=True;" Microsoft.EntityFrameworkCore.SqlServer -OutputDir Models ``` 如果希望为 `DbContext` 指定一个自定义名称,例如 `CompanyContext`,可以添加 `-Context` 参数: ```powershell Scaffold-DbContext "Server=(localdb)\mssqllocaldb;Database=Company;Trusted_Connection=True;" Microsoft.EntityFrameworkCore.SqlServer -OutputDir Models -Context CompanyContext ``` ### 配置 DbContext 生成的 `DbContext` 类会包含对数据库中每个表的 `DbSet<TEntity>` 属性,表示可以查询和保存的实体集合。此外,`OnConfiguring` 方法中会包含数据库连接字符串的配置。可以根据需要修改连接字符串或添加其他配置选项,例如使用日志记录或更改数据库提供程序。 ### 使用生成代码 生成的实体类和 `DbContext` 可以直接用于数据访问操作。例如,可以创建 `CompanyContext` 实例并查询数据库中的数据: ```csharp using (var context = new CompanyContext()) { var employees = context.Employees.ToList(); } ``` ### 注意事项 - 数据库优先模式适用于已有数据库结构的情况,但 Microsoft 推荐使用代码优先(Code First)模式进行开发,因为这种方式更符合现代开发实践,并且更容易进行版本控制和迁移管理[^1]。 - EF Core 支持多种数据库,包括 SQL Server、SQLite、PostgreSQL 等[^5]。在使用 `Scaffold-DbContext` 命令时,需要确保使用正确的数据库提供程序。 --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值