Appearance
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 Tools | Visual 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.sql3. 逆向工程生成代码
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
fi2. 使用部分方法扩展
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 Tools | 5分钟 | 低 | 低 | 小-中 |
| Devart | 30分钟 | 中 | 中 | 中-大 |
| DbSchema | 1小时 | 中 | 低 | 大 |
| T4 模板 | 4小时+ | 高 | 高 | 大 |
常见问题
Q1: 如何处理模型变更?
A: 有三种策略:
csharp
// 策略 1: 完全重新生成(适用于小项目)
dotnet ef dbcontext scaffold ... --force
// 策略 2: 手动合并(适用于中型项目)
// 1. 生成到新文件夹
// 2. 使用 diff 工具比较
// 3. 手动合并变更
// 策略 3: 混合方式(适用于大型项目)
// - 核心实体手动维护
// - 辅助表自动生