diff --git a/Extensions/FreeSql.Extensions.EFModel/EFModelExtensions.cs b/Extensions/FreeSql.Extensions.EFModel/EFModelExtensions.cs
new file mode 100644
index 000000000..e56d1c2d5
--- /dev/null
+++ b/Extensions/FreeSql.Extensions.EFModel/EFModelExtensions.cs
@@ -0,0 +1,141 @@
+using FreeSql;
+using FreeSql.Internal;
+using FreeSql.Internal.CommonProvider;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata;
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Data;
+using System.Data.Common;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+using DbContext = Microsoft.EntityFrameworkCore.DbContext;
+
+public static partial class EFModelExtensions
+{
+ ///
+ /// 根据 EFCore DbContext ModelBuilder 配置 FreeSql 实体特性
+ ///
+ ///
+ ///
+ public static ICodeFirst ApplyConfigurationFromEFCore(this ICodeFirst codeFirst, params Type[] dbContextTypes)
+ {
+ var util = (codeFirst as CodeFirstProvider)._commonUtils;
+ var globalFilters = typeof(GlobalFilter).GetField("_filters", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(util._orm.GlobalFilter) as ConcurrentDictionary;
+ var globalFiltersIndex = 55100;
+ string QuoteSqlName(string name) => util.QuoteSqlName(name.Replace(".", "_-_dot_-_")).Replace("_-_dot_-_", ".");
+ foreach (var type in dbContextTypes)
+ {
+ if (type == null) throw new ArgumentNullException(nameof(dbContextTypes));
+ if (!typeof(DbContext).IsAssignableFrom(type)) throw new ArgumentException($"类型 {type.FullName} 不是 DbContext");
+ var dbContext = Activator.CreateInstance(type);
+ if (dbContext == null) throw new InvalidOperationException($"无法创建 DbContext 实例: {type.FullName}");
+ using (dbContext as IDisposable)
+ {
+ var dbContextModel = ((DbContext)dbContext).Model;
+ var defaultSchema = dbContextModel.GetDefaultSchema();
+ foreach (var entityEf in dbContextModel.GetEntityTypes())
+ {
+ if (entityEf.IsOwned()) continue;
+ var queryFilter = entityEf.GetQueryFilter();
+ if (queryFilter != null)
+ {
+ var globalFilterName = $"efcore_{++globalFiltersIndex}";
+ var globalFilterItem = new GlobalFilter.Item();
+ typeof(GlobalFilter.Item).GetProperty("Id").SetValue(globalFilterItem, globalFiltersIndex);
+ typeof(GlobalFilter.Item).GetProperty("Name").SetValue(globalFilterItem, globalFilterName);
+ typeof(GlobalFilter.Item).GetProperty("Where").SetValue(globalFilterItem, queryFilter);
+ typeof(GlobalFilter.Item).GetProperty("Only").SetValue(globalFilterItem, true);
+ globalFilters.TryAdd(globalFilterName, globalFilterItem);
+ }
+ codeFirst.Entity(entityEf.ClrType, entity =>
+ {
+ var schema = entityEf.GetSchema();
+ if (string.IsNullOrWhiteSpace(schema)) schema = defaultSchema;
+ var tbname = entityEf.GetTableName() ?? entityEf.GetViewName();
+ entity.ToTable($"{(string.IsNullOrWhiteSpace(schema) ? $"{QuoteSqlName(schema)}." : "")}{QuoteSqlName(tbname)}");
+ var pk = entityEf.FindPrimaryKey();
+ if (pk != null) entity.HasKey(string.Join(",", pk.Properties.Select(a => a.PropertyInfo.Name)));
+ var props = new List();
+ foreach (var propEf in entityEf.GetProperties())
+ {
+ if (propEf.PropertyInfo == null) continue;
+ props.Add(propEf.PropertyInfo.Name);
+ var prop = entity.Property(propEf.PropertyInfo.Name);
+ prop.HasColumnName(propEf.GetColumnName());
+ prop.HasColumnType(propEf.GetColumnType());
+ var isIdentity = propEf.ValueGenerated == ValueGenerated.OnAdd &&
+ propEf.IsKey() &&
+ (propEf.ClrType == typeof(int) || propEf.ClrType == typeof(long));
+ if (isIdentity)
+ {
+ foreach (var anno in propEf.GetAnnotations())
+ {
+ if (anno.Name.EndsWith("ValueGenerationStrategy") && anno.Value != null && anno.Value.Equals(2))
+ {
+ isIdentity = true;
+ break;
+ }
+ }
+ }
+ prop.Help().IsIdentity(isIdentity);
+ if (!propEf.IsColumnNullable()) prop.IsRequired();
+ prop.HasDefaultValueSql(propEf.GetDefaultValueSql());
+ var maxLen = propEf.GetMaxLength();
+ if (maxLen != null) prop.HasMaxLength(maxLen.Value);
+ var precision = propEf.GetPrecision();
+ var scale = propEf.GetScale();
+ if (precision != null && scale != null) prop.HasPrecision(precision.Value, scale.Value);
+ else if (precision != null) prop.HasPrecision(precision.Value);
+ else if (scale != null) prop.HasPrecision(20, scale.Value);
+ if (propEf.IsConcurrencyToken) prop.IsRowVersion();
+ //var position = propEf.GetColumnOrder();
+ //if (position != null) prop.Position((short)position.Value);
+ }
+ foreach (var prop in entityEf.ClrType.GetProperties())
+ {
+ if (props.Contains(prop.Name)) continue;
+ var isIgnore = false;
+ var setMethod = prop.GetSetMethod(true); //trytb.Type.GetMethod($"set_{p.Name}");
+ var tp = codeFirst.GetDbInfo(prop.PropertyType);
+ if (setMethod == null || (tp == null && prop.PropertyType.IsValueType)) // 属性没有 set自动忽略
+ isIgnore = true;
+ if (tp == null && isIgnore == false) continue; //导航属性
+ entity.Property(prop.Name).Help().IsIgnore(true);
+ }
+ var navsEf = entityEf.GetNavigations();
+ foreach (var navEf in navsEf)
+ {
+ if (navEf.ForeignKey.DeclaringEntityType.IsOwned()) continue;
+ if (navEf.IsCollection)
+ {
+ var navFluent = entity.HasMany(navEf.Name);
+ if (navEf.Inverse != null)
+ {
+ if (navEf.Inverse.IsCollection)
+ navFluent.WithMany(navEf.Inverse.Name, typeof(int));
+ else
+ navFluent.WithOne(navEf.Inverse.Name).HasForeignKey(string.Join(",", navEf.Inverse.ForeignKey.Properties.Select(a => a.Name)));
+ }
+ }
+ else
+ {
+ var navFluent = entity.HasOne(navEf.Name);
+ if (navEf.Inverse != null)
+ {
+ if (navEf.Inverse.IsCollection)
+ navFluent.WithMany(navEf.Inverse.Name).HasForeignKey(string.Join(",", navEf.Inverse.ForeignKey.Properties.Select(a => a.Name)));
+ else
+ navFluent.WithOne(navEf.Inverse.Name, string.Join(",", navEf.Inverse.ForeignKey.Properties.Select(a => a.Name))).HasForeignKey(string.Join(",", navEf.ForeignKey.Properties.Select(a => a.Name)));
+ }
+ }
+ }
+ });
+ }
+ }
+ }
+ return codeFirst;
+ }
+}
diff --git a/Extensions/FreeSql.Extensions.EFModel/FreeSql.Extensions.EFModel.csproj b/Extensions/FreeSql.Extensions.EFModel/FreeSql.Extensions.EFModel.csproj
new file mode 100644
index 000000000..fbdc0ba27
--- /dev/null
+++ b/Extensions/FreeSql.Extensions.EFModel/FreeSql.Extensions.EFModel.csproj
@@ -0,0 +1,57 @@
+
+
+
+ net9.0;net8.0;net7.0;net6.0;
+ true
+ FreeSql;ncc;YeXiangQin
+ FreeSql 扩展包,聚合根(实现室).
+ https://github.com/dotnetcore/FreeSql/wiki/%E8%81%9A%E5%90%88%E6%A0%B9%EF%BC%88%E5%AE%9E%E9%AA%8C%E5%AE%A4%EF%BC%89
+ https://github.com/dotnetcore/FreeSql/wiki/%E8%81%9A%E5%90%88%E6%A0%B9%EF%BC%88%E5%AE%9E%E9%AA%8C%E5%AE%A4%EF%BC%89
+ git
+ MIT
+ FreeSql;ORM
+ $(AssemblyName)
+ logo.png
+ $(AssemblyName)
+ true
+ true
+ true
+ key.snk
+ false
+ 3.5.204-preview20250425
+ readme.md
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ FreeSql.Extensions.EFModel.xml
+ 3
+
+
+
+ net40
+
+
+
diff --git a/Extensions/FreeSql.Extensions.EFModel/key.snk b/Extensions/FreeSql.Extensions.EFModel/key.snk
new file mode 100644
index 000000000..e580bc8d5
Binary files /dev/null and b/Extensions/FreeSql.Extensions.EFModel/key.snk differ
diff --git a/FreeSql-DbContext.sln b/FreeSql-DbContext.sln
index b1b0fe45a..d407dd385 100644
--- a/FreeSql-DbContext.sln
+++ b/FreeSql-DbContext.sln
@@ -15,6 +15,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FreeSql.Tests.DbContext2",
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FreeSql.Extensions.AggregateRoot", "Extensions\FreeSql.Extensions.AggregateRoot\FreeSql.Extensions.AggregateRoot.csproj", "{B8F84E4F-46F2-4048-B79B-49F0B8A95335}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FreeSql.Extensions.ZeroEntity", "Extensions\FreeSql.Extensions.ZeroEntity\FreeSql.Extensions.ZeroEntity.csproj", "{9F309623-51D0-9A59-9FA9-0AE166E30B0C}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FreeSql.Extensions.EFModel", "Extensions\FreeSql.Extensions.EFModel\FreeSql.Extensions.EFModel.csproj", "{A05882CE-3E20-40C0-AE2E-9736848198BE}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -45,6 +49,14 @@ Global
{B8F84E4F-46F2-4048-B79B-49F0B8A95335}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B8F84E4F-46F2-4048-B79B-49F0B8A95335}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B8F84E4F-46F2-4048-B79B-49F0B8A95335}.Release|Any CPU.Build.0 = Release|Any CPU
+ {9F309623-51D0-9A59-9FA9-0AE166E30B0C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {9F309623-51D0-9A59-9FA9-0AE166E30B0C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {9F309623-51D0-9A59-9FA9-0AE166E30B0C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {9F309623-51D0-9A59-9FA9-0AE166E30B0C}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A05882CE-3E20-40C0-AE2E-9736848198BE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A05882CE-3E20-40C0-AE2E-9736848198BE}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A05882CE-3E20-40C0-AE2E-9736848198BE}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A05882CE-3E20-40C0-AE2E-9736848198BE}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/FreeSql.DbContext/EfCoreFluentApi/EfCoreFluentApiExtensions.cs b/FreeSql.DbContext/EfCoreFluentApi/EfCoreFluentApiExtensions.cs
index f09367a41..386101f2f 100644
--- a/FreeSql.DbContext/EfCoreFluentApi/EfCoreFluentApiExtensions.cs
+++ b/FreeSql.DbContext/EfCoreFluentApi/EfCoreFluentApiExtensions.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
@@ -6,6 +7,7 @@ using System.Reflection;
using FreeSql;
using FreeSql.DataAnnotations;
using FreeSql.Extensions.EfCoreFluentApi;
+using FreeSql.Internal;
using FreeSql.Internal.CommonProvider;
partial class FreeSqlDbContextExtensions
@@ -135,4 +137,5 @@ partial class FreeSqlDbContextExtensions
}
}
#endif
+
}
diff --git a/FreeSql.Tests/FreeSql.Tests.DbContext2/EFCoreFluentApiTest.cs b/FreeSql.Tests/FreeSql.Tests.DbContext2/EFCoreFluentApiTest.cs
new file mode 100644
index 000000000..06db84598
--- /dev/null
+++ b/FreeSql.Tests/FreeSql.Tests.DbContext2/EFCoreFluentApiTest.cs
@@ -0,0 +1,479 @@
+using FreeSql;
+using FreeSql.Internal;
+using FreeSql.Internal.CommonProvider;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Metadata.Internal;
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using System.Linq;
+using System.Linq.Expressions;
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Security.Cryptography;
+using System.Text;
+using System.Threading.Tasks;
+using System.Xml.Linq;
+using Xunit;
+using YamlDotNet.Serialization;
+using YamlDotNet.Serialization.NamingConventions;
+using static Microsoft.EntityFrameworkCore.Tests.FreeSqlFluentApi.EFCoreFluentApiTest;
+
+namespace Microsoft.EntityFrameworkCore.Tests.FreeSqlFluentApi
+{
+ public class EFCoreFluentApiTest
+ {
+ [Fact]
+ public void EFCoreToFreeSql()
+ {
+ using (var fsql = g.CreateMemory())
+ {
+ fsql.CodeFirst.ApplyConfigurationFromEFCore(typeof(BloggingContext));
+ fsql.Insert(new Blog()).ExecuteAffrows();
+ fsql.Select().ToList();
+ }
+ }
+
+ #region 测试 DbContext
+ public class BloggingContext : Microsoft.EntityFrameworkCore.DbContext
+ {
+ public DbSet Blogs => Set();
+ public DbSet Posts => Set();
+ public DbSet Tags => Set();
+ public DbSet Comments => Set();
+ public DbSet Users => Set();
+ //public DbSet DetailedPosts => Set(); // TPH 需要 DbSet
+ public DbSet BlogHeaders => Set(); // 无键实体类型 DbSet
+
+ public BloggingContext() : base() { }
+
+ // 配置数据库连接 (如果不用 Startup.cs 或 DI 注入)
+ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
+ {
+ if (!optionsBuilder.IsConfigured)
+ {
+ optionsBuilder.UseSqlite("data source=:memory:");
+ // 启用敏感数据日志记录(仅用于开发)
+ optionsBuilder.EnableSensitiveDataLogging();
+ }
+ }
+
+ // Fluent API 配置的核心方法
+ protected override void OnModelCreating(ModelBuilder modelBuilder)
+ {
+ // --- 全局配置 (Model-Wide Configuration) ---
+
+ // 设置默认 Schema (如果数据库支持)
+ modelBuilder.HasDefaultSchema("blogging");
+
+ // 定义数据库序列 (Sequence)
+ modelBuilder.HasSequence("BlogIdSequence", schema: "shared")
+ .StartsAt(1000)
+ .IncrementsBy(5);
+
+ // --- 实体级别配置 (Entity Configuration) ---
+
+ // 配置 Blog 实体
+ modelBuilder.Entity(entity =>
+ {
+ // 映射到特定的表名和 Schema (覆盖默认 Schema)
+ entity.ToTable("BlogInfo", "dbo");
+
+ // 配置主键 (Primary Key) - EF Core 通常能自动发现名为 Id 或 Id 的属性
+ entity.HasKey(b => b.BlogId);
+ // .HasName("PK_BlogInfo"); // 自定义主键约束名
+
+ // 使用序列生成主键值 (需要数据库支持,如 SQL Server, PostgreSQL)
+ // entity.Property(b => b.BlogId)
+ // .HasDefaultValueSql("NEXT VALUE FOR shared.BlogIdSequence"); // 使用上面定义的序列
+
+ // 配置属性 (Property Configuration)
+ entity.Property(b => b.Url)
+ .IsRequired() // 设置为必需 (NOT NULL)
+ .HasMaxLength(250) // 设置最大长度
+ .HasColumnName("BlogUrl"); // 映射到不同的列名
+
+ entity.Property(b => b.Name)
+ .HasColumnType("nvarchar(150)"); // 指定数据库列类型
+
+ entity.Property(b => b.Rating)
+ .HasPrecision(18, 2); // 设置 decimal 的精度和小数位数
+
+ // 配置并发标记 (Concurrency Token) - RowVersion/Timestamp
+ entity.Property(b => b.RowVersion)
+ .IsRowVersion(); // 效果同 [Timestamp]
+
+ // 忽略不需要映射到数据库的属性
+ entity.Ignore(b => b.LoadedFromDatabase);
+
+ // 配置索引 (Index)
+ entity.HasIndex(b => b.Url)
+ .IsUnique() // 创建唯一索引
+ .HasDatabaseName("IX_Blog_Url"); // 自定义索引名称
+
+ // 配置全局查询筛选器 (Global Query Filter) - 例如用于软删除
+ entity.HasQueryFilter(b => !b.IsDeleted);
+
+ // 配置数据库生成的默认值
+ entity.Property(b => b.CreationTimestamp)
+ .HasDefaultValueSql("current_timestamp"); // 使用数据库函数设置默认值
+ // .HasDefaultValue(new DateTime(2000, 1, 1)); // 使用固定值设置默认值
+
+ // 配置检查约束 (Check Constraint) - 需要数据库支持
+ entity.HasCheckConstraint("CK_Blog_Rating", "[Rating] >= 0 AND [Rating] <= 5", c => c.HasName("CK_BlogRatingRange"));
+
+ // 配置值转换 (Value Conversion) - 将 Uri 对象与数据库中的 string 相互转换
+ entity.Property(b => b.SiteUri)
+ .HasConversion(
+ v => v == null ? null : v.ToString(), // 模型 -> 数据库
+ v => v == null ? null : new Uri(v) // 数据库 -> 模型
+ )
+ .HasMaxLength(500); // 转换后的 string 类型需要指定长度
+
+ // 配置自有类型 (Owned Type) - AuditInfo
+ // 默认情况下,自有类型的属性会映射到拥有者实体的主表中,列名为 "OwnedTypeName_PropertyName"
+ entity.OwnsOne(b => b.AuditInfo, ownedNavigationBuilder =>
+ {
+ ownedNavigationBuilder.Property(a => a.CreatedAt).HasColumnName("DateCreated"); // 自定义列名
+ ownedNavigationBuilder.Property(a => a.UpdatedAt).HasColumnName("DateModified");
+ // ownedNavigationBuilder.ToTable("BlogAudit"); // 也可以将自有类型映射到单独的表
+ });
+
+ // --- 关系配置 (Relationship Configuration) ---
+
+ // 配置与 Post 的一对多关系 (Blog 有多个 Post)
+ // EF Core 通常能自动发现导航属性并配置关系,但这里显式配置以作演示
+ entity.HasMany(b => b.Posts) // Blog 有多个 Posts
+ .WithOne(p => p.Blog) // 每个 Post 属于一个 Blog
+ .HasForeignKey(p => p.BlogId) // Post 中的外键是 BlogId
+ .IsRequired() // 外键是必需的 (关联的 Blog 不能为 null)
+ .OnDelete(DeleteBehavior.Cascade); // 当 Blog 删除时,级联删除其 Posts
+
+ // 配置与 User 的一对多关系 (一个 User 拥有多个 Blog)
+ // 这里外键在 Blog 实体中 (OwnerId)
+ entity.HasOne(b => b.Owner) // 每个 Blog 有一个 Owner (User)
+ .WithMany(u => u.OwnedBlogs) // 一个 User 有多个 OwnedBlogs
+ .HasForeignKey(b => b.OwnerId) // Blog 表中的外键是 OwnerId
+ .IsRequired()
+ .OnDelete(DeleteBehavior.Restrict); // 当 User 删除时,如果其还拥有 Blog,则阻止删除 User
+ });
+
+ // 配置 Post 实体
+ modelBuilder.Entity(entity =>
+ {
+ entity.ToTable("Posts"); // 使用默认 Schema "blogging"
+
+ entity.HasKey(p => p.PostId);
+
+ entity.Property(p => p.Title)
+ .IsRequired()
+ .HasMaxLength(200);
+
+ entity.Property(p => p.Content)
+ .HasColumnType("ntext"); // 适用于旧 SQL Server,新版建议 nvarchar(max)
+
+ // 配置枚举到字符串的转换 (Value Conversion)
+ entity.Property(p => p.Status)
+ .HasConversion() // 将枚举存储为字符串
+ .HasMaxLength(20); // 设定存储字符串的长度
+ // 或者转换为 int: .HasConversion()
+
+ // 配置与 Blog 的多对一关系 (显式配置,虽然通常可省略)
+ entity.HasOne(p => p.Blog)
+ .WithMany(b => b.Posts)
+ .HasForeignKey(p => p.BlogId); // OnDelete 行为已在 Blog 端配置
+
+ // 配置与 User (Author) 的多对一关系 (外键可空)
+ entity.HasOne(p => p.Author)
+ .WithMany(u => u.AuthoredPosts)
+ .HasForeignKey(p => p.AuthorId)
+ .IsRequired(false) // 外键 AuthorId 可以为 NULL
+ .OnDelete(DeleteBehavior.ClientSetNull); // 当 Author 删除时,将 Post 的 AuthorId 设为 NULL (客户端行为)
+ // .OnDelete(DeleteBehavior.SetNull); // 数据库行为 (如果数据库支持)
+
+ // 配置与 Comment 的一对多关系
+ entity.HasMany(p => p.Comments)
+ .WithOne(c => c.Post)
+ .HasForeignKey(c => c.PostId)
+ .OnDelete(DeleteBehavior.Cascade);
+
+ // 配置与 Tag 的多对多关系 (Many-to-Many)
+ entity.HasMany(p => p.Tags)
+ .WithMany(t => t.Posts)
+ // EF Core 6+ 可以自动创建连接表。如果需要自定义连接表:
+ .UsingEntity>( // 使用字典类型作为连接实体
+ "PostTagLink", // 连接表名称
+ j => j.HasOne().WithMany().HasForeignKey("TagForeignKey").HasPrincipalKey(t => t.TagId), // 配置 Tag 端
+ j => j.HasOne().WithMany().HasForeignKey("PostForeignKey").HasPrincipalKey(p => p.PostId), // 配置 Post 端
+ j =>
+ {
+ j.Property("LinkedDate").HasDefaultValueSql("GETUTCDATE()"); // 连接表中的附加列
+ j.HasKey("PostForeignKey", "TagForeignKey"); // 定义连接表的复合主键
+ j.ToTable("PostTagMap", "linking"); // 自定义连接表的名称和 Schema
+ });
+ // 也可以使用显式连接实体类 `PostTag` 并配置两个一对多关系来替代 `UsingEntity`
+
+ // 配置复合索引 (Composite Index)
+ entity.HasIndex(p => new { p.BlogId, p.Title })
+ .HasDatabaseName("IX_Post_BlogId_Title");
+
+ // --- 继承映射配置 (Inheritance Mapping) ---
+ // TPH (Table Per Hierarchy) 是默认策略
+ // 需要配置鉴别器 (Discriminator) 列
+ //entity.HasDiscriminator("PostType") // 鉴别器列名和类型
+ // .HasValue("StandardPost") // 基类的鉴别器值
+ // .HasValue("DetailedBlogPost"); // 派生类的鉴别器值
+
+ // 如果是 TPT (Table Per Type)
+ // modelBuilder.Entity().ToTable("Posts");
+ // modelBuilder.Entity().ToTable("DetailedPosts"); // 派生类映射到单独的表
+
+ // 如果是 TPC (Table Per Concrete Type) - EF Core 7+
+ // modelBuilder.Entity().UseTpcMappingStrategy()
+ // .ToTable("StandardPosts");
+ // modelBuilder.Entity().UseTpcMappingStrategy()
+ // .ToTable("DetailedBlogPosts");
+ // 注意:TPC 不支持数据库生成的主键策略,如 Identity 或 Sequence。
+ });
+
+ // 配置 DetailedPost 实体 (派生类)
+ //modelBuilder.Entity(entity =>
+ //{
+ // // TPH 模式下,属性默认映射到基类表中
+ // entity.Property(dp => dp.Metadata).HasMaxLength(500);
+ // entity.Property(dp => dp.Summary).HasColumnType("nvarchar(max)");
+
+ // // TPT 模式下需要配置与基类的关系 (主键既是主键也是外键)
+ // // entity.HasOne(dp => dp.Blog).WithMany().HasForeignKey(dp => dp.BlogId); // TPT 时可能需要
+ //});
+
+
+ // 配置 Tag 实体
+ modelBuilder.Entity(entity =>
+ {
+ entity.ToTable("Tags", "taxonomy"); // 自定义 Schema
+
+ // 配置字符串主键
+ entity.HasKey(t => t.TagId);
+ entity.Property(t => t.TagId).HasMaxLength(50); // 字符串主键通常需要指定长度
+
+ entity.Property(t => t.Name)
+ .IsRequired()
+ .HasMaxLength(100);
+
+ entity.HasIndex(t => t.Name)
+ .IsUnique(); // 标签名唯一
+ });
+
+ // 配置 Comment 实体
+ modelBuilder.Entity(entity =>
+ {
+ // 使用默认表名 "Comments" 和默认 Schema "blogging"
+ entity.HasKey(c => c.CommentId);
+
+ entity.Property(c => c.Text).IsRequired();
+
+ entity.Property(c => c.PostedDate)
+ .HasDefaultValueSql("GETUTCDATE()");
+ });
+
+ // 配置 User 实体
+ modelBuilder.Entity(entity =>
+ {
+ entity.ToTable("ApplicationUsers", "identity"); // 自定义表名和 Schema
+
+ entity.HasKey(u => u.UserId);
+
+ entity.Property(u => u.Username)
+ .IsRequired()
+ .HasMaxLength(100);
+
+ entity.HasIndex(u => u.Username)
+ .IsUnique(); // 用户名唯一
+
+ entity.Property(u => u.Email)
+ .HasMaxLength(254); // Email 标准长度
+
+ entity.HasIndex(u => u.Email)
+ .IsUnique()
+ .HasFilter("[Email] IS NOT NULL"); // 可空列上的唯一索引 (SQL Server 特定语法)
+
+ // 配置自有类型 (Owned Type) - Address
+ // 映射到与 User 相同的表中 (默认)
+ entity.OwnsOne(u => u.ShippingAddress, owned =>
+ {
+ // 可以为自有类型的列名添加前缀或自定义
+ owned.Property(a => a.Street).HasColumnName("ShippingStreet").HasMaxLength(200);
+ owned.Property(a => a.City).HasColumnName("ShippingCity").HasMaxLength(100);
+ owned.Property(a => a.PostCode).HasColumnName("ShippingPostCode").HasMaxLength(20);
+ owned.Property(a => a.Country).HasColumnName("ShippingCountry").HasMaxLength(100);
+
+ // 如果需要将 Address 映射到单独的表 "UserAddresses"
+ // owned.ToTable("UserAddresses", "identity");
+ // owned.WithOwner().HasForeignKey("OwnerUserId"); // 需要显式配置外键
+ });
+ });
+
+ // 配置无键实体类型 (Keyless Entity Type / Query Type)
+ modelBuilder.Entity(entity =>
+ {
+ entity.HasNoKey(); // 明确指出没有主键
+
+ // 映射到数据库视图
+ entity.ToView("vw_BlogHeader", "dbo");
+
+ // 或者映射到 SQL 查询 (EF Core 5+)
+ // entity.ToSqlQuery("SELECT Name, Url FROM Blogs WHERE IsDeleted = 0");
+ });
+
+ // --- Seed Data (填充种子数据) ---
+ // modelBuilder.Entity().HasData(
+ // new Tag { TagId = "tech", Name = "Technology" },
+ // new Tag { TagId = "efcore", Name = "Entity Framework Core" },
+ // new Tag { TagId = "dotnet", Name = ".NET" }
+ // );
+
+ // modelBuilder.Entity().HasData(
+ // new User { UserId = 1, Username = "AdminUser", Email = "admin@example.com" }
+ // );
+
+ // modelBuilder.Entity().HasData(
+ // new Blog { BlogId = 1, Name = "EF Core Blog", Url = "http://blogs.msdn.com/efcore", Rating = 5, OwnerId = 1, IsDeleted = false, CreationTimestamp = DateTime.UtcNow, AuditInfo = new AuditInfo { CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow } }
+ //);
+
+ // 注意:HasData 对于有复杂关系或自有类型的种子数据配置可能比较麻烦,有时需要手动插入。
+ // 对于 AuditInfo 这样的 Owned Type,EF Core 7+ 支持在 HasData 中直接初始化。
+
+ // 调用基类方法 (如果基类有 OnModelCreating 实现)
+ base.OnModelCreating(modelBuilder);
+ }
+ }
+
+ public class Blog
+ {
+ public int BlogId { get; set; } // 主键 (Primary Key)
+ public string? Name { get; set; } // 可空字符串
+ public string Url { get; set; } = null!; // 必需字符串
+ public decimal Rating { get; set; } // Decimal 类型
+ public bool IsDeleted { get; set; } // 用于软删除的标志
+ public DateTime CreationTimestamp { get; set; } // 创建时间戳
+ public Uri? SiteUri { get; set; } // 演示值转换
+
+ // 并发标记 (Concurrency Token)
+ [Timestamp] // DataAnnotation 方式, Fluent API 也能配
+ public byte[]? RowVersion { get; set; }
+
+ // 导航属性 (Navigation Properties)
+ public List Posts { get; set; } = new List(); // 一对多 (Blog -> Post)
+
+ // 外键属性 (Foreign Key Property) - 可选,EF Core 可自动生成影子属性
+ public int OwnerId { get; set; }
+ public User Owner { get; set; } = null!; // 一对多 (User -> Blog)
+
+ // 自有类型 (Owned Type) - 嵌入式
+ public AuditInfo AuditInfo { get; set; } = new AuditInfo();
+
+ // 计算属性或非映射属性 (Ignored Property)
+ public bool LoadedFromDatabase { get; set; }
+ }
+
+ public class Post
+ {
+ public int PostId { get; set; }
+ public string Title { get; set; } = null!;
+ public string? Content { get; set; }
+ public PostStatus Status { get; set; } // 枚举类型,演示值转换
+
+ // 外键属性
+ public int BlogId { get; set; }
+ public Blog Blog { get; set; } = null!; // 多对一 (Post -> Blog)
+
+ public int? AuthorId { get; set; } // 可空外键
+ public User? Author { get; set; } // 多对一 (Post -> User)
+
+ // 导航属性
+ public List Comments { get; set; } = new List(); // 一对多 (Post -> Comment)
+ public List Tags { get; set; } = new List(); // 多对多 (Post <-> Tag)
+
+ // 用于 TPH 继承演示
+ // public string PostType { get; set; } // Discriminator (鉴别器), EF Core 会自动添加
+ }
+
+ // 继承示例 (TPH - Table Per Hierarchy)
+ //public class DetailedPost : Post
+ //{
+ // public string? Metadata { get; set; }
+ // public string? Summary { get; set; }
+ //}
+
+ public class Tag
+ {
+ public string TagId { get; set; } = null!; // 字符串主键
+ public string Name { get; set; } = null!;
+
+ // 导航属性
+ public List Posts { get; set; } = new List(); // 多对多 (Tag <-> Post)
+ }
+
+ public class Comment
+ {
+ public int CommentId { get; set; }
+ public string Text { get; set; } = null!;
+ public DateTime PostedDate { get; set; }
+
+ // 外键属性
+ public int PostId { get; set; }
+ public Post Post { get; set; } = null!; // 多对一 (Comment -> Post)
+ }
+
+ public class User
+ {
+ public int UserId { get; set; }
+ public string Username { get; set; } = null!;
+ public string? Email { get; set; } // 可空,可能有唯一约束
+
+ // 导航属性
+ public List OwnedBlogs { get; set; } = new List(); // 一对多 (User -> Blog)
+ public List AuthoredPosts { get; set; } = new List(); // 一对多 (User -> Post)
+
+ // 自有类型 (Owned Type) - 可以映射到同一张表或不同表
+ public Address? ShippingAddress { get; set; }
+ }
+
+ // 自有类型 (Owned Type) / 值对象 (Value Object)
+ [Owned] // 可以用 DataAnnotation 标记,也可以完全用 Fluent API 配置
+ public class AuditInfo
+ {
+ public DateTime CreatedAt { get; set; }
+ public DateTime UpdatedAt { get; set; }
+ }
+
+ [Owned]
+ public class Address
+ {
+ public string Street { get; set; } = null!;
+ public string City { get; set; } = null!;
+ public string PostCode { get; set; } = null!;
+ public string Country { get; set; } = null!;
+ }
+
+ // 无键实体类型 (Keyless Entity Type) / 查询类型 (Query Type)
+ // 通常用于映射数据库视图或存储过程/函数结果
+ public class BlogHeader
+ {
+ public string? Name { get; set; }
+ public string Url { get; set; } = null!;
+ }
+
+ public enum PostStatus
+ {
+ Draft,
+ Published,
+ Archived
+ }
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/FreeSql.Tests/FreeSql.Tests.DbContext2/FreeSql.Tests.DbContext2.csproj b/FreeSql.Tests/FreeSql.Tests.DbContext2/FreeSql.Tests.DbContext2.csproj
index 0c4164e05..6e2a3ff86 100644
--- a/FreeSql.Tests/FreeSql.Tests.DbContext2/FreeSql.Tests.DbContext2.csproj
+++ b/FreeSql.Tests/FreeSql.Tests.DbContext2/FreeSql.Tests.DbContext2.csproj
@@ -1,7 +1,7 @@
- net5.0
+ net8.0
false
@@ -11,16 +11,19 @@
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
diff --git a/FreeSql/FreeSql.xml b/FreeSql/FreeSql.xml
index befb26a3b..dc17b3bf3 100644
--- a/FreeSql/FreeSql.xml
+++ b/FreeSql/FreeSql.xml
@@ -1097,93 +1097,6 @@
-
-
- 动态创建实体类型
-
-
-
-
- 配置Class
-
- 类名
- 类标记的特性[Table(Name = "xxx")] [Index(xxxx)]
-
-
-
-
- 获取类型构建器,可作为要构建的Type来引用
-
-
-
-
- 配置属性
-
- 属性名称
- 属性类型
- 属性标记的特性-支持多个
-
-
-
-
- 配置属性
-
- 属性名称
- 属性类型
- 该属性是否重写父类属性
- 属性标记的特性-支持多个
-
-
-
-
- 配置属性
-
- 属性名称
- 属性类型
- 该属性是否重写父类属性
- 属性默认值
- 属性标记的特性-支持多个
-
-
-
-
- 配置父类
-
- 父类类型
-
-
-
-
- Override属性
-
-
-
-
-
- Emit动态创建出Class - Type
-
-
-
-
-
- Emit动态创建出Class - Type,不附带获取TableInfo
-
-
-
-
-
- 首字母小写
-
-
-
-
-
-
- 首字母大写
-
-
-
-
获取实体的主键值,以 "*|_,[,_|*" 分割,当任意一个主键属性无值时,返回 ""
@@ -5984,28 +5897,6 @@
对象池
-
-
- 动态构建Class Type
-
-
-
-
-
- 根据字典,创建 table 对应的实体对象
-
-
-
-
-
-
-
- 根据实体对象,创建 table 对应的字典
-
-
-
-
-
C#: that >= between && that <= and