2014-01-08 3 views
2

Я пытаюсь создать журнал аудита любых изменений в точке сохранения с помощью Entity Framework. До сих пор у меня он работает довольно хорошо, сохраняя все изменения, сделанные в каждой области, используя код ниже:Как вы можете получить значения внешнего ключа из DBEntityEntry?

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() 
     }); 
    } 
} 

Проблема я столкнулся, как получить значения для любых внешних ключей, которые принадлежат к этому объекту. Например: у меня есть объект транспортного средства, который имеет отношения к серии поисковых таблиц, таких как коробка передач, модель и т. Д. Если эти значения изменятся, таблица аудита сохранит измененный идентификатор, но я хочу сохранить фактическое значение.

Есть ли способ получить значение внешнего ключа в этой ситуации?

ответ

1

Alrighty ... это старый вопрос, но я провел последнее, работая над этим, потому что у меня были такие же требования. Может быть, есть более простой способ, но вот код, который я использовал:

Вашего исходный код, слегка модифицирован для моих целей (RecordID это всегда целый), и вызвать новый метод для расчета нового значения

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 (обратите внимание на код по умолчанию будет свойство под названием «Имя», если он не находит)

[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; 
    } 
0

Это решение основано на @JamesR's answer.

Моя цель состояла в том, чтобы сделать код более общим, чтобы он мог использоваться для нескольких внешних ключей, подключающихся к различным таблицам.

Улучшения стоит отметить:

  • Я переместил код, который получает список внешних ключей вне цикла ИмениСвойства foreach. Поскольку список FK не изменяется в зависимости от конкретного свойства, нет никаких оснований каждый раз получать новый список. Если в системе много FK, это может занять некоторое время, поэтому вы не захотите повторять процесс без необходимости.

  • Вместо жесткого кодирования определенного типа класса, как GetType(typeof(User), я извлек внешних ключей таблицы из ФК с помощью:

    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(); 
} 
Смежные вопросы