【问题标题】:Entity Framework Auditing using ObjectContext on SaveChanges在 SaveChanges 上使用 ObjectContext 进行实体框架审计
【发布时间】:2014-10-30 22:43:08
【问题描述】:

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

  • [User12 - 12/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;

有一次,我知道EntryMaterial 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 = new MaterialAuditData(); 公共无效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;
    }

【问题讨论】:

    标签: c# database entity-framework


    【解决方案1】:

    我遇到了同样的问题。

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

    DbContext.Set(entityType).Find(id)
    

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

    一些选项是:

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

    假设:

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

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

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

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

      • 您已启用延迟加载(我强烈建议您不要这样做……但是……这会有所帮助)

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

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

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

    但是,您的审核要求可能会更严格。例如,如果有人更改了 FK 模型上的名称/描述,那么这似乎是在修改旧的审计日志。

    【讨论】:

      猜你喜欢
      • 2012-09-06
      • 1970-01-01
      • 2015-08-24
      • 1970-01-01
      • 1970-01-01
      • 2011-04-18
      • 2017-04-03
      • 2013-10-31
      • 1970-01-01
      相关资源
      最近更新 更多