在SaveChanges上使用ObjectContext进行实体框架审核

本文关键字:实体 框架 ObjectContext SaveChanges | 更新日期: 2023-09-27 18:28:27

为了进行审计日志记录,我需要获取所有列的值,包括为数据库中的某个表修改的FK实体和关系实体。数据库基本上是为用户可以上传资源(文件、在线文档、图片等)的网站而建的,我有一个名为Material的表,它有多个多个二元和一个二元的关系,如Material - AudienceMaterial - Category、"材料上传器"、"材料权限Material -Tags等。我想记录Material发生的所有更改。例如,如果有人从材料中删除标签,那么我需要记录:

  • [User12-12/12]-Happy标签已从Crappy材料中删除

到目前为止,我得到的是:我可以使用获得所有修改、添加、删除的ObjectStateEntries

context.ObjectStateManager.GetObjectStateEntries(EntityState.Added | EntityState.Deleted | EntityState.Modified)

现在,我可以使用检查这个ObjectStateEntry是否是RelationShip

if (e.IsRelationship) {
    HandleRelationshipEntry(e);
}
else {
    HandleEntry(e);
}

HandleEntry方法中(条目不是关系条目),我可以检查Entry的类型,在我的情况下是Material,所以我正在做:

// We care about only Material which are modifed
if (e.State != EntityState.Modified || !(e.Entity is Material))
    return;

一旦我知道Entry的类型是Material Entry,我就可以使用获取Material表中所有更改的列

e.CurrentValues[ARCHIVE_COLUMN].ToString() != e.OriginalValues[ARCHIVE_COLUMN].ToString()

此时,我可以记录Material表的所有非FK更改。但如果列是FK到其他entity,我就无法将该FK值解析为相应的Entity。我只知道CategoryID已经从42更改为76,但我无法解析Category本身的名称。我尝试了将DBDataRecordCurrentValueRecord转换为EntityKey的方法,但它只是NULL。是否有任何方法可以使用ObjectStateManager来解决这些FKsEntities

我的完整代码供参考:

专用类SingleMaterialLogger{MaterialAuditData auditData=新材料auditData();public void HandleEntity(ObjectStateEntry e,ObjectContext上下文){HandlePrimaryTypeChanges(e);HandleComplexTypeChanges(e,context);}

        private void HandleComplexTypeChanges(ObjectStateEntry e, ObjectContext c) {
            // Owner, Category, Contact
            ChangeValueHelper(e, CONTACT_COLUMN, (k1, k2) => {
                // get old value
                User old = c.GetObjectByKey(k1) as User;
                User current = c.GetObjectByKey(k2) as User;
            });
        }
        public void HandlePrimaryTypeChanges(ObjectStateEntry e) {
            // Name, Description, ArchiveDate, Status
            // Again no reflection is used - So change them if column name changes
            ChangeValueHelper<string>(e, NAME_COLUMN, (change) => auditData.Name = change);
            ChangeValueHelper<string>(e, NAME_COLUMN, (change) => auditData.Description = change);
            // TODO - Fix change value helper
            if (e.CurrentValues[ARCHIVE_COLUMN].ToString() != e.OriginalValues[ARCHIVE_COLUMN].ToString()) {
                auditData.ArchiveDate = new Change<DateTime?>(e.OriginalValues[ARCHIVE_COLUMN] as DateTime?, e.CurrentValues[ARCHIVE_COLUMN] as DateTime?);
            }
        }
        private void ChangeValueHelper(ObjectStateEntry e, string columnName, Action<EntityKey, EntityKey> func) {
            if (e.CurrentValues[columnName].ToString() != e.OriginalValues[columnName].ToString()) {
                func(e.OriginalValues[columnName] as EntityKey, e.CurrentValues[columnName] as EntityKey);
            }
        }
        private void ChangeValueHelper<T>(ObjectStateEntry e, string columnName, Action<Change<T>> func) where T : class {
            if(e.CurrentValues[columnName].ToString() != e.OriginalValues[columnName].ToString()) {
                func(new Change<T>(e.OriginalValues[columnName] as T, e.OriginalValues[columnName] as T));
            }
        }
    }

    Dictionary<EntityKey, SingleMaterialLogger> singleMaterialLoggerMap = new Dictionary<EntityKey, SingleMaterialLogger>();
    private ObjectContext context;
    public MaterialAuditLogger(ObjectContext context) {
        this.context = context;
    }
    public void AuditMaterialChanges() {
        // Grab everything thats being added/deleted/modified
        foreach(var e in context.ObjectStateManager.GetObjectStateEntries(EntityState.Added | EntityState.Deleted | EntityState.Modified)) {
            if (e.IsRelationship) {
                HandleRelationshipEntity(e);
            }
            else {
                HandleEntity(e);
            }
        }
    }
    private void HandleEntity(ObjectStateEntry e) {
        // We care about only Material which are modifed
        if (e.State != EntityState.Modified || !(e.Entity is Material))
            return;
        var logger = SingleLogger(e.EntityKey);
        logger.HandleEntity(e, context);
    }
    private void HandleRelationshipEntity(ObjectStateEntry e) {
        // relations whose entity keys contains
    }
    private SingleMaterialLogger SingleLogger(EntityKey key) {
        if(singleMaterialLoggerMap.ContainsKey(key))
            return singleMaterialLoggerMap[key];
        SingleMaterialLogger logger = new SingleMaterialLogger();
        singleMaterialLoggerMap[key] = logger;
        return logger;
    }

在SaveChanges上使用ObjectContext进行实体框架审核

我遇到了同样的问题。

提取任何具有id值的实体类型并不困难:

DbContext.Set(entityType).Find(id)

但是,这假设您首先已经从相关导航属性中标识了实体类型。这需要一些技巧,基本上是通过使用反射来查看属性名称和[ForeignKey()]属性等来复制EF逻辑。

一些选项包括:

1) 添加智能以从FK ID属性中计算出FK模型属性。然后在审核日志创建过程中动态查找FK模型,并将.ToString()值存储在审核日志中。

这假设:

  • DataContext/Restore中有一个通用的实用程序,可以动态查找任何模型类型(例如DbContext.Set(entityType).Find(id))

  • 由于以下原因之一,您确信所有FK模型上的.ToString()实现将可靠地工作:

    • 他们从不依赖可能导致运行时错误的进一步导航属性

    • 您可以确信,在您的模型查找中正确地包含了进一步的导航属性()

    • 你已经启用了延迟加载(我强烈建议不要这样……但……这在这里会有所帮助)

  • 您已经考虑了交易的含义(如果您使用的交易超出了EF的范围)

2) 将FK ID存储在审核日志中。然后,在查看审核日志时,动态查找FK模型,并在屏幕上渲染ToString()。

我们在项目中采用了这个选项,效果很好。

但是,您的审核要求可能更严格。例如,如果有人更改FK模型上的名称/描述,则会出现修改旧审核日志的情况。