You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.26 08:44:33