如何用EntityFrameworkCore配置单表多对多关系及CMS用户操作追踪实现
一、实现后端内容操作活动追踪模块
针对.NET Core 2 + EF Core的场景,我们可以通过重写DbContext的SaveChanges方法自动捕获实体的创建/更新/删除操作,生成活动日志。下面是具体实现步骤:
1. 定义活动日志实体
先创建ActivityLog类,记录关键操作信息:
public class ActivityLog { public int Id { get; set; } public string OperationType { get; set; } // 例如 "Create", "Update", "Delete" public DateTime OperationTime { get; set; } public string OperatorId { get; set; } // 操作者ID,类型可根据你的认证体系调整 public string EntityType { get; set; } // 操作的实体类型,比如 "Article", "Page" public int EntityId { get; set; } // 操作的实体ID public string ChangeDetails { get; set; } // 可选:记录变更详情,比如JSON格式的新旧值对比 }
2. 在DbContext中注册实体
把ActivityLog添加到你的DbContext里:
public class YourDbContext : DbContext { public DbSet<ActivityLog> ActivityLogs { get; set; } // 你的其他DbSet... // 保持你现有构造函数的实现 public YourDbContext(DbContextOptions<YourDbContext> options) : base(options) { } }
3. 重写SaveChanges方法捕获操作
在DbContext中重写同步/异步SaveChanges方法,自动生成活动日志:
public override int SaveChanges() { TrackActivityChanges(); return base.SaveChanges(); } public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default) { TrackActivityChanges(); return await base.SaveChangesAsync(cancellationToken); } private void TrackActivityChanges() { // 获取当前登录用户ID,这里假设用HttpContext获取,需和你的认证方式匹配 var operatorId = HttpContext?.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value; operatorId ??= "Anonymous"; // 未登录时设为匿名,可根据需求调整 foreach (var entry in ChangeTracker.Entries()) { if (entry.Entity is ActivityLog) continue; // 避免追踪日志自身的变化 var entityType = entry.Entity.GetType().Name; var entityId = int.Parse(entry.Property("Id").CurrentValue.ToString()); switch (entry.State) { case EntityState.Added: AddActivityLog("Create", operatorId, entityType, entityId); break; case EntityState.Modified: var changeDetails = GetChangeDetails(entry); AddActivityLog("Update", operatorId, entityType, entityId, changeDetails); break; case EntityState.Deleted: AddActivityLog("Delete", operatorId, entityType, entityId); break; } } } private void AddActivityLog(string operationType, string operatorId, string entityType, int entityId, string changeDetails = null) { ActivityLogs.Add(new ActivityLog { OperationType = operationType, OperationTime = DateTime.UtcNow, OperatorId = operatorId, EntityType = entityType, EntityId = entityId, ChangeDetails = changeDetails }); } private string GetChangeDetails(EntityEntry entry) { // 简单实现:记录所有修改属性的新旧值 var changes = new Dictionary<string, object>(); foreach (var prop in entry.Properties.Where(p => p.IsModified)) { changes.Add(prop.Metadata.Name, new { OldValue = prop.OriginalValue, NewValue = prop.CurrentValue }); } return JsonConvert.SerializeObject(changes); // 需要引用Newtonsoft.Json }
注意:获取用户ID的逻辑要和你的项目认证体系匹配(比如JWT、Cookie认证),如果是后台服务调用,可能需要通过参数传递操作者信息。
4. 迁移数据库
运行EF Core迁移命令创建ActivityLogs表:
Add-Migration AddActivityLogs Update-Database
二、单个表配置多个多对多关系(EF Core 2.x)
EF Core 2.x不支持隐式多对多关系,必须显式创建连接实体。假设我们有一个Content表,需要同时和Tag、Category两个表建立多对多关系,具体实现如下:
1. 定义实体和连接实体
// 主实体:内容 public class Content { public int Id { get; set; } public string Title { get; set; } // 与Tag的多对多关联(通过连接实体) public ICollection<ContentTag> ContentTags { get; set; } = new List<ContentTag>(); // 与Category的多对多关联(通过连接实体) public ICollection<ContentCategory> ContentCategories { get; set; } = new List<ContentCategory>(); } // 连接实体1:内容-标签 public class ContentTag { public int ContentId { get; set; } public Content Content { get; set; } public int TagId { get; set; } public Tag Tag { get; set; } } // 标签实体 public class Tag { public int Id { get; set; } public string Name { get; set; } public ICollection<ContentTag> ContentTags { get; set; } = new List<ContentTag>(); } // 连接实体2:内容-分类 public class ContentCategory { public int ContentId { get; set; } public Content Content { get; set; } public int CategoryId { get; set; } public Category Category { get; set; } } // 分类实体 public class Category { public int Id { get; set; } public string Name { get; set; } public ICollection<ContentCategory> ContentCategories { get; set; } = new List<ContentCategory>(); }
2. 在DbContext中配置关系
在OnModelCreating方法中,为每个多对多关系配置复合主键和外键:
protected override void OnModelCreating(ModelBuilder modelBuilder) { // 配置ContentTag的复合主键 modelBuilder.Entity<ContentTag>() .HasKey(ct => new { ct.ContentId, ct.TagId }); // 配置Content与ContentTag的关联 modelBuilder.Entity<ContentTag>() .HasOne(ct => ct.Content) .WithMany(c => c.ContentTags) .HasForeignKey(ct => ct.ContentId); // 配置Tag与ContentTag的关联 modelBuilder.Entity<ContentTag>() .HasOne(ct => ct.Tag) .WithMany(t => t.ContentTags) .HasForeignKey(ct => ct.TagId); // 配置ContentCategory的复合主键 modelBuilder.Entity<ContentCategory>() .HasKey(cc => new { cc.ContentId, cc.CategoryId }); // 配置Content与ContentCategory的关联 modelBuilder.Entity<ContentCategory>() .HasOne(cc => cc.Content) .WithMany(c => c.ContentCategories) .HasForeignKey(cc => cc.ContentId); // 配置Category与ContentCategory的关联 modelBuilder.Entity<ContentCategory>() .HasOne(cc => cc.Category) .WithMany(cat => cat.ContentCategories) .HasForeignKey(cc => cc.CategoryId); // 你的其他实体配置... }
3. 使用多对多关系
现在可以通过连接实体操作多对多关系:
// 给内容添加标签 var content = new Content { Title = "测试文章" }; var tag = new Tag { Name = "技术" }; context.ContentTags.Add(new ContentTag { Content = content, Tag = tag }); await context.SaveChangesAsync(); // 查询内容的所有标签 var contentWithTags = await context.Contents .Include(c => c.ContentTags) .ThenInclude(ct => ct.Tag) .FirstOrDefaultAsync(c => c.Id == 1);
内容的提问来源于stack exchange,提问作者Sebbo
相关产品推荐
相关产品推荐

