- 增加 FreeSql.Extensions.EFModel 从 efcore modelBuilder FluentApi 同步到 IFreeSql;

This commit is contained in:
2881099
2025-04-28 14:50:31 +08:00
parent 2ba3c85133
commit 50f883c0de
8 changed files with 696 additions and 110 deletions

View File

@@ -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
{
/// <summary>
/// 根据 EFCore DbContext ModelBuilder 配置 FreeSql 实体特性
/// </summary>
/// <param name="codeFirst"></param>
/// <param name="dbContextTypes"></param>
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<string, GlobalFilter.Item>;
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<string>();
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;
}
}

View File

@@ -0,0 +1,57 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net9.0;net8.0;net7.0;net6.0;</TargetFrameworks>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<Authors>FreeSql;ncc;YeXiangQin</Authors>
<Description>FreeSql 扩展包,聚合根(实现室).</Description>
<PackageProjectUrl>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</PackageProjectUrl>
<RepositoryUrl>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</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageTags>FreeSql;ORM</PackageTags>
<PackageId>$(AssemblyName)</PackageId>
<PackageIcon>logo.png</PackageIcon>
<Title>$(AssemblyName)</Title>
<IsPackable>true</IsPackable>
<GenerateAssemblyInfo>true</GenerateAssemblyInfo>
<SignAssembly>true</SignAssembly>
<AssemblyOriginatorKeyFile>key.snk</AssemblyOriginatorKeyFile>
<DelaySign>false</DelaySign>
<Version>3.5.204-preview20250425</Version>
<PackageReadmeFile>readme.md</PackageReadmeFile>
</PropertyGroup>
<ItemGroup>
<None Include="../../readme.md" Pack="true" PackagePath="\" />
<None Include="../../logo.png" Pack="true" PackagePath="\" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net9.0'">
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.0" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.0" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net7.0'">
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.0" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net6.0'">
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\FreeSql.DbContext\FreeSql.DbContext.csproj" />
</ItemGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|netstandard2.0|AnyCPU'">
<DocumentationFile>FreeSql.Extensions.EFModel.xml</DocumentationFile>
<WarningLevel>3</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition="'$(TargetFramework)' == 'net40'">
<DefineConstants>net40</DefineConstants>
</PropertyGroup>
</Project>

Binary file not shown.

View File

@@ -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

View File

@@ -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
}

View File

@@ -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<Blog>().ToList();
}
}
#region DbContext
public class BloggingContext : Microsoft.EntityFrameworkCore.DbContext
{
public DbSet<Blog> Blogs => Set<Blog>();
public DbSet<Post> Posts => Set<Post>();
public DbSet<Tag> Tags => Set<Tag>();
public DbSet<Comment> Comments => Set<Comment>();
public DbSet<User> Users => Set<User>();
//public DbSet<DetailedPost> DetailedPosts => Set<DetailedPost>(); // TPH 需要 DbSet
public DbSet<BlogHeader> BlogHeaders => Set<BlogHeader>(); // 无键实体类型 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<int>("BlogIdSequence", schema: "shared")
.StartsAt(1000)
.IncrementsBy(5);
// --- 实体级别配置 (Entity Configuration) ---
// 配置 Blog 实体
modelBuilder.Entity<Blog>(entity =>
{
// 映射到特定的表名和 Schema (覆盖默认 Schema)
entity.ToTable("BlogInfo", "dbo");
// 配置主键 (Primary Key) - EF Core 通常能自动发现名为 Id 或 <TypeName>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<Post>(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<string>() // 将枚举存储为字符串
.HasMaxLength(20); // 设定存储字符串的长度
// 或者转换为 int: .HasConversion<int>()
// 配置与 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<Dictionary<string, object>>( // 使用字典类型作为连接实体
"PostTagLink", // 连接表名称
j => j.HasOne<Tag>().WithMany().HasForeignKey("TagForeignKey").HasPrincipalKey(t => t.TagId), // 配置 Tag 端
j => j.HasOne<Post>().WithMany().HasForeignKey("PostForeignKey").HasPrincipalKey(p => p.PostId), // 配置 Post 端
j =>
{
j.Property<DateTime>("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<string>("PostType") // 鉴别器列名和类型
// .HasValue<Post>("StandardPost") // 基类的鉴别器值
// .HasValue<DetailedPost>("DetailedBlogPost"); // 派生类的鉴别器值
// 如果是 TPT (Table Per Type)
// modelBuilder.Entity<Post>().ToTable("Posts");
// modelBuilder.Entity<DetailedPost>().ToTable("DetailedPosts"); // 派生类映射到单独的表
// 如果是 TPC (Table Per Concrete Type) - EF Core 7+
// modelBuilder.Entity<Post>().UseTpcMappingStrategy()
// .ToTable("StandardPosts");
// modelBuilder.Entity<DetailedPost>().UseTpcMappingStrategy()
// .ToTable("DetailedBlogPosts");
// 注意TPC 不支持数据库生成的主键策略,如 Identity 或 Sequence。
});
// 配置 DetailedPost 实体 (派生类)
//modelBuilder.Entity<DetailedPost>(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<Tag>(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<Comment>(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<User>(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<BlogHeader>(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<Tag>().HasData(
// new Tag { TagId = "tech", Name = "Technology" },
// new Tag { TagId = "efcore", Name = "Entity Framework Core" },
// new Tag { TagId = "dotnet", Name = ".NET" }
// );
// modelBuilder.Entity<User>().HasData(
// new User { UserId = 1, Username = "AdminUser", Email = "admin@example.com" }
// );
// modelBuilder.Entity<Blog>().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 TypeEF 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<Post> Posts { get; set; } = new List<Post>(); // 一对多 (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<Comment> Comments { get; set; } = new List<Comment>(); // 一对多 (Post -> Comment)
public List<Tag> Tags { get; set; } = new List<Tag>(); // 多对多 (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<Post> Posts { get; set; } = new List<Post>(); // 多对多 (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<Blog> OwnedBlogs { get; set; } = new List<Blog>(); // 一对多 (User -> Blog)
public List<Post> AuthoredPosts { get; set; } = new List<Post>(); // 一对多 (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
}
}

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
@@ -11,16 +11,19 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.4" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="YamlDotNet" Version="16.3.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Extensions\FreeSql.Extensions.AggregateRoot\FreeSql.Extensions.AggregateRoot.csproj" />
<ProjectReference Include="..\..\Extensions\FreeSql.Extensions.EFModel\FreeSql.Extensions.EFModel.csproj" />
<ProjectReference Include="..\..\FreeSql.DbContext\FreeSql.DbContext.csproj" />
<ProjectReference Include="..\..\FreeSql.Repository\FreeSql.Repository.csproj" />
<ProjectReference Include="..\..\Providers\FreeSql.Provider.Sqlite\FreeSql.Provider.Sqlite.csproj" />

View File

@@ -1097,93 +1097,6 @@
</summary>
<returns></returns>
</member>
<member name="T:FreeSql.Extensions.DynamicEntity.DynamicCompileBuilder">
<summary>
动态创建实体类型
</summary>
</member>
<member name="M:FreeSql.Extensions.DynamicEntity.DynamicCompileBuilder.#ctor(IFreeSql,System.String,System.Attribute[])">
<summary>
配置Class
</summary>
<param name="className">类名</param>
<param name="attributes">类标记的特性[Table(Name = "xxx")] [Index(xxxx)]</param>
<returns></returns>
</member>
<member name="P:FreeSql.Extensions.DynamicEntity.DynamicCompileBuilder.TypeBuilder">
<summary>
获取类型构建器可作为要构建的Type来引用
</summary>
</member>
<member name="M:FreeSql.Extensions.DynamicEntity.DynamicCompileBuilder.Property(System.String,System.Type,System.Attribute[])">
<summary>
配置属性
</summary>
<param name="propertyName">属性名称</param>
<param name="propertyType">属性类型</param>
<param name="attributes">属性标记的特性-支持多个</param>
<returns></returns>
</member>
<member name="M:FreeSql.Extensions.DynamicEntity.DynamicCompileBuilder.Property(System.String,System.Type,System.Boolean,System.Attribute[])">
<summary>
配置属性
</summary>
<param name="propertyName">属性名称</param>
<param name="propertyType">属性类型</param>
<param name="isOverride">该属性是否重写父类属性</param>
<param name="attributes">属性标记的特性-支持多个</param>
<returns></returns>
</member>
<member name="M:FreeSql.Extensions.DynamicEntity.DynamicCompileBuilder.Property(System.String,System.Type,System.Boolean,System.Object,System.Attribute[])">
<summary>
配置属性
</summary>
<param name="propertyName">属性名称</param>
<param name="propertyType">属性类型</param>
<param name="isOverride">该属性是否重写父类属性</param>
<param name="defaultValue">属性默认值</param>
<param name="attributes">属性标记的特性-支持多个</param>
<returns></returns>
</member>
<member name="M:FreeSql.Extensions.DynamicEntity.DynamicCompileBuilder.Extend(System.Type)">
<summary>
配置父类
</summary>
<param name="superClass">父类类型</param>
<returns></returns>
</member>
<member name="M:FreeSql.Extensions.DynamicEntity.DynamicCompileBuilder.OverrideProperty(System.Reflection.Emit.TypeBuilder@,System.Reflection.Emit.MethodBuilder,FreeSql.Extensions.DynamicEntity.DynamicCompileBuilder.PropertyMethodEnum,System.String)">
<summary>
Override属性
</summary>
<param name="typeBuilder"></param>
</member>
<member name="M:FreeSql.Extensions.DynamicEntity.DynamicCompileBuilder.Build">
<summary>
Emit动态创建出Class - Type
</summary>
<returns></returns>
</member>
<member name="M:FreeSql.Extensions.DynamicEntity.DynamicCompileBuilder.BuildJustType">
<summary>
Emit动态创建出Class - Type不附带获取TableInfo
</summary>
<returns></returns>
</member>
<member name="M:FreeSql.Extensions.DynamicEntity.DynamicCompileBuilder.FirstCharToLower(System.String)">
<summary>
首字母小写
</summary>
<param name="input"></param>
<returns></returns>
</member>
<member name="M:FreeSql.Extensions.DynamicEntity.DynamicCompileBuilder.FirstCharToUpper(System.String)">
<summary>
首字母大写
</summary>
<param name="input"></param>
<returns></returns>
</member>
<member name="M:FreeSql.Extensions.EntityUtil.EntityUtilExtensions.GetEntityKeyString(IFreeSql,System.Type,System.Object,System.Boolean,System.String)">
<summary>
获取实体的主键值,以 "*|_,[,_|*" 分割,当任意一个主键属性无值时,返回 ""
@@ -5984,28 +5897,6 @@
对象池
</summary>
</member>
<member name="M:FreeSqlGlobalDynamicEntityExtensions.DynamicEntity(FreeSql.ICodeFirst,System.String,System.Attribute[])">
<summary>
动态构建Class Type
</summary>
<returns></returns>
</member>
<member name="M:FreeSqlGlobalDynamicEntityExtensions.CreateInstance(FreeSql.Internal.Model.TableInfo,System.Collections.Generic.Dictionary{System.String,System.Object})">
<summary>
根据字典,创建 table 对应的实体对象
</summary>
<param name="table"></param>
<param name="dict"></param>
<returns></returns>
</member>
<member name="M:FreeSqlGlobalDynamicEntityExtensions.CreateDictionary(FreeSql.Internal.Model.TableInfo,System.Object)">
<summary>
根据实体对象,创建 table 对应的字典
</summary>
<param name="table"></param>
<param name="instance"></param>
<returns></returns>
</member>
<member name="M:FreeSqlGlobalExpressionCallExtensions.Between(System.DateTime,System.DateTime,System.DateTime)">
<summary>
C# that >= between &amp;&amp; that &lt;= and<para></para>