Skip to content

Model-First 可视化设计 ​

概述 ​

Model-First(模型优先)是一种通过可视化工具设计数据模型,然后生成数据库的开发模式。开发人员使用图形化界面(Entity Data Model Designer)绘制实体、属性和关系,EF Core 根据模型生成数据库架构和 C# 代码。

⚠️ EF Core 现状说明 ​

重要: 传统的 Model-First 工作流主要在 EF 6.x (Entity Framework) 中支持,使用 .edmx 文件进行可视化设计。EF Core 目前不直接支持可视化的 Model-First 工具。

但是,我们可以通过以下方式实现类似的 Model-First 体验:

方案描述成熟度
EF Power ToolsVisual Studio 扩展,提供逆向工程和模型查看功能✅ 成熟
Devart Entity Developer第三方可视化建模工具,支持 EF Core✅ 成熟
DbSchema / ER/Studio数据库设计工具,可生成 EF Core 兼容的模型✅ 成熟
手动创建 T4 模板基于自定义 DSL 生成代码⚠️ 复杂
未来计划Microsoft 正在开发新的可视化设计器🚧 开发中

适用场景 ​

✅ 团队协作: 与业务人员沟通,可视化展示数据结构
✅ 复杂关系建模: 直观地设计多对多、继承等复杂关系
✅ 文档化: 自动生成 ER 图和数据库文档
✅ 教学演示: 学习 EF Core 概念时的可视化工具
❌ 简单项目: 直接使用 Code-First 更高效
❌ 敏捷迭代: 频繁变更时维护模型成本较高


方案一: EF Power Tools ​

安装 ​

bash
# Visual Studio Marketplace
# 1. 打开 Visual Studio
# 2. 扩展 -> 管理扩展
# 3. 搜索 "EF Core Power Tools"
# 4. 下载安装并重启 VS

# 或通过命令行安装
vsixinstaller /q EFCorePowerTools.vsix

功能特性 ​

功能说明
逆向工程从现有数据库生成模型和 DbContext
模型查看器可视化查看已生成的实体关系图
DbContext 拆分将大型 DbContext 拆分为多个配置文件
DacFx 集成生成数据库架构脚本
图表导出导出为 PNG/SVG 格式

使用步骤 ​

1. 从数据库生成模型 ​

1. 右键点击项目
2. 选择 "EF Core Power Tools" -> "Reverse Engineer"
3. 配置连接字符串
4. 选择要生成的表和视图
5. 配置选项:
   - Use legacy pluralizer (复数化命名)
   - Include connection string in generated code
   - Generate separate configuration files
6. 点击 OK 生成代码

2. 查看可视化模型图 ​

1. 在解决方案资源管理器中
2. 双击生成的 .png 或 .dgml 文件
3. 查看实体关系图
4. 可以缩放、拖拽、搜索

3. 导出模型图 ​

1. 右键点击设计器表面
2. 选择 "Export" -> "Export as Image"
3. 选择格式: PNG / SVG / BMP
4. 保存到文件

方案二: Devart Entity Developer ​

安装 ​

bash
# 1. 下载 Devart Entity Developer
# https://www.devart.com/entitydeveloper.html

# 2. 安装 NuGet 包
dotnet add package Devart.Data.SQLite.EFCore

创建新模型 ​

步骤 1: 启动设计器 ​

1. 打开 Devart Entity Developer
2. 文件 -> 新建模型
3. 选择 "EF Core Model"
4. 保存为 .edml 文件

步骤 2: 设计实体 ​

csharp
// 在设计器中:
// 1. 右键 -> 添加实体
// 2. 输入实体名称: Product
// 3. 添加标量属性:
//    - Id (int, Identity)
//    - Name (string, MaxLength=100)
//    - Price (decimal, Precision=18, Scale=2)
//    - CreatedAt (DateTime)
// 4. 设置主键: Id

步骤 3: 设计关系 ​

csharp
// 一对多关系: Category -> Products
// 1. 创建 Category 实体
// 2. 拖拽 "关联" 工具
// 3. 从 Category 拖到 Product
// 4. 配置关系:
//    - Multiplicity: One to Many
//    - Navigation Property: Products (in Product), Category (in Category)
//    - Foreign Key: CategoryId

步骤 4: 生成代码 ​

1. 右键点击设计器表面
2. 选择 "Generate Database"
3. 配置连接字符串
4. 预览生成的 SQL 脚本
5. 执行脚本创建数据库
6. 生成 C# 代码

生成的代码示例 ​

csharp
// Product.cs
using System;
using System.Collections.Generic;

namespace MyStore.Models
{
    public partial class Product
    {
        public int Id { get; set; }
        public string Name { get; set; } = null!;
        public decimal Price { get; set; }
        public DateTime CreatedAt { get; set; }
        public int? CategoryId { get; set; }
        
        // 导航属性
        public virtual Category? Category { get; set; }
    }
}

// Category.cs
using System;
using System.Collections.Generic;

namespace MyStore.Models
{
    public partial class Category
    {
        public Category()
        {
            Products = new HashSet<Product>();
        }
        
        public int Id { get; set; }
        public string Name { get; set; } = null!;
        public string? Description { get; set; }
        
        // 导航属性
        public virtual ICollection<Product> Products { get; set; }
    }
}

// StoreContext.cs
using Microsoft.EntityFrameworkCore;

namespace MyStore.Models
{
    public partial class StoreContext : DbContext
    {
        public StoreContext()
        {
        }

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

        public virtual DbSet<Category> Categories { get; set; }
        public virtual DbSet<Product> Products { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Category>(entity =>
            {
                entity.HasKey(e => e.Id);
                
                entity.Property(e => e.Name)
                    .IsRequired()
                    .HasMaxLength(100);
                    
                entity.Property(e => e.Description)
                    .HasMaxLength(500);
            });

            modelBuilder.Entity<Product>(entity =>
            {
                entity.HasKey(e => e.Id);
                
                entity.Property(e => e.Name)
                    .IsRequired()
                    .HasMaxLength(100);
                    
                entity.Property(e => e.Price)
                    .HasColumnType("decimal(18,2)");
                    
                entity.Property(e => e.CreatedAt)
                    .HasDefaultValueSql("GETDATE()");
                
                entity.HasOne(d => d.Category)
                    .WithMany(p => p.Products)
                    .HasForeignKey(d => d.CategoryId)
                    .OnDelete(DeleteBehavior.Cascade);
            });

            OnModelCreatingPartial(modelBuilder);
        }

        partial void OnModelCreatingPartial(ModelBuilder modelBuilder);
    }
}

方案三: 使用 DbSchema 设计 ​

工作流程 ​

┌─────────────────┐
│  1. DbSchema 设计 │
│     ER 图        │
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│  2. 生成 DDL     │
│     SQL 脚本     │
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│  3. 执行 DDL     │
│     创建数据库    │
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│  4. 逆向工程     │
│     EF Scaffolding│
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│  5. 生成 C# 代码 │
│     实体类       │
└─────────────────┘

详细步骤 ​

1. 在 DbSchema 中设计模型 ​

sql
-- DbSchema 生成的 DDL 脚本
-- categories.sql
CREATE TABLE Categories (
    Id INT PRIMARY KEY IDENTITY(1,1),
    Name NVARCHAR(100) NOT NULL,
    Description NVARCHAR(500),
    CreatedAt DATETIME2 DEFAULT GETDATE()
);

-- products.sql
CREATE TABLE Products (
    Id INT PRIMARY KEY IDENTITY(1,1),
    Name NVARCHAR(100) NOT NULL,
    Price DECIMAL(18,2) NOT NULL,
    StockQuantity INT NOT NULL DEFAULT 0,
    CategoryId INT,
    CreatedAt DATETIME2 DEFAULT GETDATE(),
    
    CONSTRAINT FK_Products_Categories_CategoryId
        FOREIGN KEY (CategoryId) REFERENCES Categories(Id)
        ON DELETE CASCADE
);

-- orders.sql
CREATE TABLE Orders (
    Id INT PRIMARY KEY IDENTITY(1,1),
    OrderNumber NVARCHAR(50) NOT NULL UNIQUE,
    CustomerEmail NVARCHAR(255) NOT NULL,
    TotalAmount DECIMAL(18,2) NOT NULL,
    Status NVARCHAR(20) NOT NULL DEFAULT 'Pending',
    OrderDate DATETIME2 DEFAULT GETDATE(),
    ShippedDate DATETIME2 NULL
);

-- order_items.sql
CREATE TABLE OrderItems (
    Id INT PRIMARY KEY IDENTITY(1,1),
    OrderId INT NOT NULL,
    ProductId INT NOT NULL,
    Quantity INT NOT NULL,
    UnitPrice DECIMAL(18,2) NOT NULL,
    
    CONSTRAINT FK_OrderItems_Orders_OrderId
        FOREIGN KEY (OrderId) REFERENCES Orders(Id)
        ON DELETE CASCADE,
        
    CONSTRAINT FK_OrderItems_Products_ProductId
        FOREIGN KEY (ProductId) REFERENCES Products(Id)
        ON DELETE NO ACTION
);

-- 索引
CREATE INDEX IX_Products_CategoryId ON Products(CategoryId);
CREATE INDEX IX_OrderItems_OrderId ON OrderItems(OrderId);
CREATE INDEX IX_OrderItems_ProductId ON OrderItems(ProductId);
CREATE INDEX IX_Orders_Status ON Orders(Status);

2. 执行 DDL 创建数据库 ​

bash
# 使用 SQL Server Management Studio
# 1. 连接到服务器
# 2. 新建查询
# 3. 粘贴 DDL 脚本
# 4. 执行

# 或使用命令行
sqlcmd -S localhost -d MyStore -i schema.sql

3. 逆向工程生成代码 ​

bash
dotnet ef dbcontext scaffold \
  "Server=localhost;Database=MyStore;Trusted_Connection=True;TrustServerCertificate=True;" \
  Microsoft.EntityFrameworkCore.SqlServer \
  --output-dir Models \
  --context StoreContext \
  --force

方案四: 手写模型定义语言(T4 模板) ​

T4 模板基础 ​

t4
<#@ template debug="false" hostspecific="false" language="C#" #>
<#@ assembly name="System.Core" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ output extension=".cs" #>

<#
// 定义实体模型
var entities = new[] {
    new {
        Name = "Product",
        Properties = new[] {
            new { Name = "Id", Type = "int", IsKey = true },
            new { Name = "Name", Type = "string", MaxLength = 100 },
            new { Name = "Price", Type = "decimal", Precision = "18,2" },
            new { Name = "CategoryId", Type = "int?", IsForeignKey = true }
        }
    },
    new {
        Name = "Category",
        Properties = new[] {
            new { Name = "Id", Type = "int", IsKey = true },
            new { Name = "Name", Type = "string", MaxLength = 100 },
            new { Name = "Description", Type = "string?", MaxLength = 500 }
        }
    }
};
#>

using System;
using System.Collections.Generic;

namespace MyStore.Models
{
<# foreach (var entity in entities) { #>
    public class <#= entity.Name #>
    {
<#     foreach (var prop in entity.Properties) { #>
        public <#= prop.Type #> <#= prop.Name #> { get; set; }
<#     } #>
    }

<# } #>
}

完整示例: 电商系统建模 ​

模型设计 ​

┌──────────────┐       ┌──────────────┐
│   Customer   │       │    Order     │
├──────────────┤       ├──────────────┤
│ Id           │───┐   │ Id           │
│ Email        │   └──>│ CustomerId   │
│ FirstName    │   1:N │ OrderDate    │
│ LastName     │       │ Status       │
│ Phone        │       │ TotalAmount  │
└──────────────┘       └──────┬───────┘
                              │
                              │ 1:N
                              │
                       ┌──────▼───────┐
                       │  OrderItem   │
                       ├──────────────┤
                       │ Id           │
                       │ OrderId      │──┐
                       │ ProductId    │  │
                       │ Quantity     │  │
                       │ UnitPrice    │  │
                       └──────────────┘  │
                                         │
                              ┌──────────┘
                              │
                        ┌─────▼──────┐
                        │  Product   │
                        ├────────────┤
                        │ Id         │
                        │ Name       │
                        │ Price      │
                        │ StockQty   │
                        │ CategoryId │──┐
                        └────────────┘  │
                                        │
                              ┌─────────┘
                              │
                        ┌─────▼──────┐
                        │  Category  │
                        ├────────────┤
                        │ Id         │
                        │ Name       │
                        │ Description│
                        └────────────┘

生成的 DbContext ​

csharp
// ECommerceContext.cs
using Microsoft.EntityFrameworkCore;

namespace ECommerce.Models
{
    public class ECommerceContext : DbContext
    {
        public ECommerceContext(DbContextOptions<ECommerceContext> options)
            : base(options)
        {
        }

        public DbSet<Customer> Customers { get; set; }
        public DbSet<Order> Orders { get; set; }
        public DbSet<OrderItem> OrderItems { get; set; }
        public DbSet<Product> Products { get; set; }
        public DbSet<Category> Categories { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            // Customer 配置
            modelBuilder.Entity<Customer>(entity =>
            {
                entity.HasKey(c => c.Id);
                
                entity.Property(c => c.Email)
                    .IsRequired()
                    .HasMaxLength(255);
                    
                entity.HasIndex(c => c.Email).IsUnique();
                
                entity.Property(c => c.FirstName)
                    .IsRequired()
                    .HasMaxLength(100);
                    
                entity.Property(c => c.LastName)
                    .IsRequired()
                    .HasMaxLength(100);
            });

            // Category 配置
            modelBuilder.Entity<Category>(entity =>
            {
                entity.HasKey(c => c.Id);
                
                entity.Property(c => c.Name)
                    .IsRequired()
                    .HasMaxLength(100);
                    
                entity.Property(c => c.Description)
                    .HasMaxLength(1000);
            });

            // Product 配置
            modelBuilder.Entity<Product>(entity =>
            {
                entity.HasKey(p => p.Id);
                
                entity.Property(p => p.Name)
                    .IsRequired()
                    .HasMaxLength(200);
                    
                entity.Property(p => p.Price)
                    .HasColumnType("decimal(18,2)");
                    
                entity.HasOne(p => p.Category)
                    .WithMany(c => c.Products)
                    .HasForeignKey(p => p.CategoryId)
                    .OnDelete(DeleteBehavior.SetNull);
                    
                entity.HasIndex(p => p.CategoryId);
            });

            // Order 配置
            modelBuilder.Entity<Order>(entity =>
            {
                entity.HasKey(o => o.Id);
                
                entity.Property(o => o.OrderNumber)
                    .IsRequired()
                    .HasMaxLength(50);
                    
                entity.HasIndex(o => o.OrderNumber).IsUnique();
                
                entity.Property(o => o.CustomerEmail)
                    .IsRequired()
                    .HasMaxLength(255);
                    
                entity.Property(o => o.Status)
                    .IsRequired()
                    .HasMaxLength(20)
                    .HasDefaultValue("Pending");
                    
                entity.Property(o => o.TotalAmount)
                    .HasColumnType("decimal(18,2)");
                    
                entity.HasOne(o => o.Customer)
                    .WithMany(c => c.Orders)
                    .HasForeignKey(o => o.CustomerId)
                    .OnDelete(DeleteBehavior.Restrict);
                    
                entity.HasIndex(o => o.CustomerId);
                entity.HasIndex(o => o.Status);
            });

            // OrderItem 配置
            modelBuilder.Entity<OrderItem>(entity =>
            {
                entity.HasKey(oi => oi.Id);
                
                entity.Property(oi => oi.Quantity)
                    .IsRequired();
                    
                entity.Property(oi => oi.UnitPrice)
                    .HasColumnType("decimal(18,2)");
                    
                entity.HasOne(oi => oi.Order)
                    .WithMany(o => o.OrderItems)
                    .HasForeignKey(oi => oi.OrderId)
                    .OnDelete(DeleteBehavior.Cascade);
                    
                entity.HasOne(oi => oi.Product)
                    .WithMany(p => p.OrderItems)
                    .HasForeignKey(oi => oi.ProductId)
                    .OnDelete(DeleteBehavior.NoAction);
                    
                entity.HasIndex(oi => oi.OrderId);
                entity.HasIndex(oi => oi.ProductId);
            });

            OnModelCreatingPartial(modelBuilder);
        }

        partial void OnModelCreatingPartial(ModelBuilder modelBuilder);
    }
}

最佳实践 ​

✅ 推荐做法 ​

1. 保持模型同步 ​

bash
# 建立自动化脚本检测数据库变更
#!/bin/bash
# sync-models.sh

# 1. 备份当前模型
cp -r Models Models.backup

# 2. 重新生成
dotnet ef dbcontext scaffold "$CONNECTION_STRING" \
  Microsoft.EntityFrameworkCore.SqlServer \
  --output-dir Models \
  --force

# 3. 比较差异
diff -r Models Models.backup

# 4. 如果无差异,恢复备份
if [ $? -eq 0 ]; then
    echo "No changes detected"
    rm -rf Models
    mv Models.backup Models
else
    echo "Models updated"
    rm -rf Models.backup
fi

2. 使用部分方法扩展 ​

csharp
// 永远不要修改生成的代码
// 使用部分方法添加自定义逻辑

public partial class StoreContext
{
    partial void OnModelCreatingPartial(ModelBuilder modelBuilder)
    {
        // 添加全局查询过滤器
        modelBuilder.Entity<Product>()
            .HasQueryFilter(p => p.StockQuantity > 0);
        
        // 添加审计配置
        modelBuilder.Entity<Order>()
            .Property(o => o.CreatedAt)
            .HasDefaultValueSql("GETDATE()");
    }
}

3. 版本控制 ​

gitignore
# .gitignore
# 忽略生成的模型文件(可选)
# Models/*.cs

# 但保留自定义扩展
Models/*Extensions.cs

❌ 避免的错误 ​

1. 直接修改生成的代码 ​

csharp
// ❌ 错误: 直接修改生成的实体类
public partial class Product
{
    public int Id { get; set; }
    
    // ⚠️ 这个属性下次重新生成时会丢失
    public string CustomField { get; set; }
}

// ✅ 正确: 使用部分类扩展
public partial class Product
{
    // 在另一个文件中添加
    public string CustomField { get; set; }
}

2. 忽略外键约束 ​

sql
-- ❌ 错误: 没有外键约束
CREATE TABLE OrderItems (
    OrderId INT,
    ProductId INT
);

-- ✅ 正确: 定义外键
CREATE TABLE OrderItems (
    OrderId INT,
    ProductId INT,
    CONSTRAINT FK_OrderItems_Products 
        FOREIGN KEY (ProductId) REFERENCES Products(Id)
);

性能对比 ​

方案初始设置时间学习曲线维护成本适合团队规模
EF Power Tools5分钟低低小-中
Devart30分钟中中中-大
DbSchema1小时中低大
T4 模板4小时+高高大

常见问题 ​

Q1: 如何处理模型变更? ​

A: 有三种策略:

csharp
// 策略 1: 完全重新生成(适用于小项目)
dotnet ef dbcontext scaffold ... --force

// 策略 2: 手动合并(适用于中型项目)
// 1. 生成到新文件夹
// 2. 使用 diff 工具比较
// 3. 手动合并变更

// 策略 3: 混合方式(适用于大型项目)
// - 核心实体手动维护
// - 辅助表自动生

基于 MIT 许可发布