如何从DBEntityEntry获取外键值?

时间:2014-01-08 09:23:30

标签: c# .net entity-framework

我正在尝试使用Entity Framework创建保存时任何更改的审核日志。到目前为止,我的工作相当不错,使用以下代码存储对每个字段所做的所有更改:

foreach (string propertyName in dbEntry.OriginalValues.PropertyNames)
{
    // For updates, we only want to capture the columns that actually changed
    if (!object.Equals(dbEntry.OriginalValues.GetValue<object>(propertyName), dbEntry.CurrentValues.GetValue<object>(propertyName)))
    {
        result.Add(new AuditLog()
        {
            UserID = UserId,
            EventDateUTC = changeTime,
            EventType = "M",    // Modified
            TableName = tableName,
            RecordID = primaryKey.ToString(),
            ColumnName = propertyName,
            OriginalValue = dbEntry.OriginalValues.GetValue<object>(propertyName) == null ? null : dbEntry.OriginalValues.GetValue<object>(propertyName).ToString(),
            NewValue = dbEntry.CurrentValues.GetValue<object>(propertyName) == null ? null : dbEntry.CurrentValues.GetValue<object>(propertyName).ToString()
        });
    }
}

我面临的问题是如何获取属于此对象的任何外键的值。例如:我有一个车辆对象与一系列查找表有关系,例如变速箱,模型等。如果这些值发生变化,审计表将存储已更改的ID,但我想存储实际值。

有没有办法在这种情况下获取外键值?

2 个答案:

答案 0 :(得分:1)

好吧......这是一个古老的问题,但我花了最后一次工作,因为我有完全相同的要求。也许有一种更简单的方法,但这里是我使用的代码:

您的原始代码,为我的目的稍作修改(RecordID始终为int),并调用新方法来计算新值

foreach (string propertyName in dbEntry.OriginalValues.PropertyNames)
            {
                // For updates, we only want to capture the columns that actually changed
                if (!Equals(dbEntry.OriginalValues.GetValue<object>(propertyName), dbEntry.CurrentValues.GetValue<object>(propertyName)))
                {
                    var newVal = getNewValueAsString(dbEntry, tableName, propertyName);

                    result.Add(new AuditLog
                                {
                                    UserID = currentUser.ID,
                                    Timestamp = changeTime,
                                    EventType = EventType.Modified,
                                    TableName = tableName,
                                    RecordID = dbEntry.OriginalValues.GetValue<int>(keyName),
                                    ColumnName = propertyName,
                                    OriginalValue = dbEntry.OriginalValues.GetValue<object>(propertyName) == null ? null : dbEntry.OriginalValues.GetValue<object>(propertyName).ToString(),
                                    NewValue = newVal
                                }
                        );
                }
            }

名为“IsName”的新属性

[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public class IsNameAttribute : Attribute
{

}

使用IsName属性标记外键模型的“name”属性(注意如果找不到代码,代码将默认为名为“Name”的属性)

[Required]
[IsName]
public string Name { get; set; }

繁重的代码

 private string getNewValueAsString(DbEntityEntry dbEntry, string tableName, string propertyName)
    {
        var fkVal = getForeignKeyValue(tableName, propertyName, dbEntry.CurrentValues.GetValue<object>(propertyName));
        return fkVal != null ? fkVal.ToString()
                            : (dbEntry.CurrentValues.GetValue<object>(propertyName) == null ? null
                                : dbEntry.CurrentValues.GetValue<object>(propertyName).ToString());
    }

    private object getForeignKeyValue(string tableName, string propertyName, object foreignKeyID)
    {
        // if this property is part of a foreign key, we need to instead look that up and store the value of the
        // foreign key

        // first get all the foreign keys in the system
        var workspace = ((IObjectContextAdapter)this).ObjectContext.MetadataWorkspace;
        var items = workspace.GetItems<AssociationType>(DataSpace.CSpace);
        if (items == null) return null;
        var fk = items.Where(a => a.IsForeignKey).ToList();
        // now we look into the FK attributes and find that the "To Role" is out current table, and the
        // "To Property" is out current property. The underscore is a bit of an assumption that the foreign
        // key name built by EF will be ENTITY_BLAH_BLAH
        var thisFk = fk.Where(x => x.ReferentialConstraints[0].ToRole.Name.StartsWith(tableName + "_"))
            .FirstOrDefault(x => x.ReferentialConstraints[0].ToProperties[0].Name == propertyName);
        // if fkname has no results, this is not a foreign key and we are done
        if (thisFk == null) return null;

        // Now that we know the foriegn key, we need to lookup the Name value in the other table

        // find the assembly
        var assembly = Assembly.GetCallingAssembly();
        // build the type for the foreign key entity
        // e.g. if the current entity is Task, and the property is StatusID, we are 
        // getting the "TaskStatus" type with reflection
        // "User" class is an object in the Models namespace - you could just hardcode the string if you want
        var foreignKeyType = assembly.GetType(typeof(User).Namespace + "." +
                  thisFk.ReferentialConstraints[0].FromRole.GetEntityType().Name);

        // get the DbSet, same as: "(new DBContext()).EntityName"
        var fkSet = Set(foreignKeyType);
        // and find the row in that table
        var fkItem = fkSet.Find(foreignKeyID);

        // find the first column marked with the "IsName" attribute, otherwise default to "Name"
        var nameColProperty = foreignKeyType.GetProperties()
            .FirstOrDefault(p => p.GetCustomAttributes(typeof(IsNameAttribute), false).Any());
        string nameCol = "Name";
        if (nameColProperty != null) nameCol = nameColProperty.Name;
        var nameColProperty2 = fkItem.GetType().GetProperty(nameCol);
        if (nameColProperty2 == null) return null;

        // get the value
        var fkValue = nameColProperty2.GetValue(fkItem, null);

        // and now, my brain hurts
        return fkValue;
    }

答案 1 :(得分:0)

此解决方案基于@JamesR's answer

我的目标是使代码更通用,以便它可以用于连接到不同表的多个外键。

值得注意的改进:

  • 我移动了获取propertyName foreach循环之外的外键列表的代码。由于FK列表不会根据特定属性而更改,因此没有理由每次都检索新列表。如果系统中有许多FK,这可能需要一段时间,因此您不希望不必要地重复该过程。

  • 我没有硬编码像GetType(typeof(User)这样的特定类类型,而是使用以下方法从FK中检索外键表名:

    string lookUpTableName = thisFk.ReferentialConstraints[0].FromRole.Name;      
    

    然后,虽然引用的FK属性名称通常是ID,但由于它可以变化,我也检索了FK属性名称:

    string lookUpPropertyName = thisFk.ReferentialConstraints[0].FromProperties[0].Name;  
    

    然后我使用ObjectContext.ExecuteStoreQuery动态插入表名和列名并检索外键文本值。

  • 如果属性是FK,我会获得原始新值的FK文本值。

完整代码:

首先,获取系统中所有外键的列表。

IObjectContextAdapter contextAdapter = ((IObjectContextAdapter)this);
MetadataWorkspace workspace = contextAdapter.ObjectContext.MetadataWorkspace;
var items = workspace.GetItems<AssociationType>(DataSpace.CSpace);

List<AssociationType> FKList = items == null ? null
    : items.Where(a => a.IsForeignKey).ToList();

然后,循环遍历属性列表,并在FK存在时用外键值替换原始值和当前值。

foreach (string propertyName in entry.OriginalValues.PropertyNames)
{
    var original = entry.OriginalValues.GetValue<object>(propertyName);
    var current = entry.CurrentValues.GetValue<object>(propertyName);

    if (FKList != null)
    {
        GetPossibleForeignKeyValues(tableName, propertyName, ref original, ref current,
            FKList, contextAdapter);
    }

    if ((original == null && current != null) ||
        (original != null && !original.Equals(current)))
    {
        result.Add(new AuditLog()
        {
            UserID = UserId,
            EventDateUTC = changeTime,
            EventType = "M",    // Modified
            TableName = tableName,
            RecordID = primaryKey.ToString(),
            ColumnName = propertyName,
            OriginalValue = original != null ? original.ToString() : "NULL",
            NewValue = current != null ? current.ToString() : "NULL"
        });
    }
}

这是实际的外键查找代码:

private void GetPossibleForeignKeyValues(string tableName, string propertyName,
    ref object originalFKValue, ref object newFKValue,
    List<AssociationType> FKList, IObjectContextAdapter contextAdapter)
{
    // If this property is part of a foreign key, look up and set the FKValue to the text
    // value of the foreign key. Otherwise, just leave the FKValue alone.

    // Look into the FK attributes and find that the "To Role" is out current table,
    // and the "To Property" is out current property.
    AssociationType thisFk = FKList.FirstOrDefault(x =>
        tableName.Contains(x.ReferentialConstraints[0].ToRole.Name)
        && propertyName.Contains(x.ReferentialConstraints[0].ToProperties[0].Name));

    // If fkname has no results, this is not a foreign key and we are done.
    if (thisFk != null)
    {
        // Now that we know the foriegn key, look up the Name value in the other table.
        string lookUpTableName = thisFk.ReferentialConstraints[0].FromRole.Name;
        string lookUpPropertyName = thisFk.ReferentialConstraints[0].FromProperties[0].Name;

        //Assuming the FK column name is "Name".
        //Use the idea in @JamesR's solution or some sort of LookUp table if it is not.
        string commandText = BuildCommandText("Name", lookUpTableName, lookUpPropertyName);

        originalFKValue = contextAdapter.ObjectContext
            .ExecuteStoreQuery<string>(commandText, new SqlParameter("FKID", originalFKValue))
            .FirstOrDefault() ?? originalFKValue;

        newFKValue = contextAdapter.ObjectContext
            .ExecuteStoreQuery<string>(commandText, new SqlParameter("FKID", newFKValue))
            .FirstOrDefault() ?? originalFKValue;

    }
}

这是我用来构建SQL CommandText的方法:

private string BuildCommandText(string columnName, string lookUpTableName, 
    string lookUpPropertyName)    
{
    StringBuilder builder = new StringBuilder();

    builder.Append("SELECT ");
    builder.Append(columnName);
    builder.Append(" FROM ");
    builder.Append(lookUpTableName);
    builder.Append(" WHERE ");
    builder.Append(lookUpPropertyName);
    builder.Append(" = @FKID");

    //The result query will look something like:
    //SELECT ColumnName FROM TableName WHERE PropertyName = @FKID

    return builder.ToString();
}