Заказчик должен регистрировать каждое изменение данных в таблице регистрации с фактическим пользователем, который внес изменение. Приложение использует одного пользователя SQL для доступа к базе данных, но нам нужно зарегистрировать «настоящий» идентификатор пользователя.
Мы можем сделать это в t-sql, написав триггеры для каждой вставки и обновления таблицы и используя context_info для хранения идентификатора пользователя. Мы передали идентификатор пользователя в хранимую процедуру, сохранили идентификатор пользователя в contextinfo, и триггер может использовать эту информацию для записи строк журнала в таблицу журнала.
Я не могу найти место или способ, где и как я могу сделать что-то подобное с помощью EF. Итак, основная цель: если я внесу изменение в данные через EF, я хотел бы записать точное изменение данных в таблицу полуавтоматическим способом (поэтому я не хочу проверять каждое поле на наличие изменений перед сохранение объекта). Мы используем EntitySQL.
К сожалению, мы должны придерживаться SQL 2000, поэтому функция отслеживания изменений данных, представленная в SQL2008, не является вариантом (но, возможно, это тоже не правильный путь для нас).
Есть идеи, ссылки или отправные точки?
[Редактировать] Некоторые примечания: используя обработчик событий ObjectContext.SavingChanges, я могу получить точку, в которой я могу ввести оператор SQL для инициализации contextinfo. Однако я не могу смешивать EF и стандартный SQL. Таким образом, я могу получить EntityConnection, но не могу выполнить с его помощью оператор T-SQL. Или я могу получить строку подключения EntityConnection и создать на ее основе SqlConnection, но это будет другое соединение, поэтому contextinfo не повлияет на сохранение, сделанное EF.
Я пробовал в обработчике SavingChanges следующее:
testEntities te = (testEntities)sender;
DbConnection dc = te.Connection;
DbCommand dcc = dc.CreateCommand();
dcc.CommandType = CommandType.StoredProcedure;
DbParameter dp = new EntityParameter();
dp.ParameterName = "userid";
dp.Value = textBox1.Text;
dcc.CommandText = "userinit";
dcc.Parameters.Add(dp);
dcc.ExecuteNonQuery();
Ошибка: значение EntityCommand.CommandText недопустимо для команды StoredProcedure. То же самое с SqlParameter вместо EntityParameter: нельзя использовать SqlParameter.
StringBuilder cStr = new StringBuilder("declare @tx char(50); set @tx='");
cStr.Append(textBox1.Text);
cStr.Append("'; declare @m binary(128); set @m = cast(@tx as binary(128)); set context_info @m;");
testEntities te = (testEntities)sender;
DbConnection dc = te.Connection;
DbCommand dcc = dc.CreateCommand();
dcc.CommandType = CommandType.Text;
dcc.CommandText = cStr.ToString();
dcc.ExecuteNonQuery();
Ошибка: неверный синтаксис запроса.
Итак, я застрял, чтобы создать мост между Entity Framework и ADO.NET. Если я смогу заставить его работать, я опубликую доказательство концепции.
Есть интересный пост по поводу аудита здесь, как вы думаете?
Шесть лет спустя, и я все еще считаю, что это отличный подход. Триггеры лучше подходят для аудита, чем код уровня приложения, и это обеспечивает отличный способ получить «настоящую» информацию о пользователе на уровне базы данных, не загрязняя код уровня приложения.
Хорошая ясная запись здесь davecallan.com/passing-userid-delete-trigger-entity-framewor k / # с использованием транзакционного подхода для обеспечения того же соединения.





Как насчет обработки Context.Сохранение изменений?
Ага, вот чего я бы хотел избежать. :-) Было бы неплохо справиться со всем этим в автоматическом режиме. У нас уже есть триггер-генератор для обработки части регистрации. Недостающее звено в том, что мы не можем передать идентификатор пользователя триггеру.
Вы не можете использовать SavingChanges для установки идентификатора пользователя в контекстной информации? msdn.microsoft.com/en-us/library/ms187768.aspx
Вот дерьмо. Самое простое решение, и мы думали о чем-то очень сложном. Спасибо, что открыли мне глаза.
Извините, мне нужно отозвать принятый статус, потому что он не работает. Соединение закрывается (отдельные объекты), и если я открываю новое соединение и заполняю context_info, это не влияет на соединение, открытое во время сохранения. :-(
Вы можете предоставить собственное соединение для использования EF. См .: msdn.microsoft.com/en-us/library/bb738540.aspx
Да, но вы не можете выполнять стандартные операторы sql с помощью EntityConnection, и вы не можете преобразовать EntityConnection в SqlConnection, поэтому я не могу смешивать эти два метода.
EntityConnection - это DbConnection, поэтому вы должны иметь возможность вызывать CreateDbCommand. Я не пробовал, но в документации это перечислено и не сказано, чтобы вы этого не делали.
Да, можно, но что с этим делать? Я не могу запустить t-sql. Я предоставлю вам больше примеров в исходном вопросе.
Хммм ... Вроде надо использовать StoreConnection (см. Ссылку). Не могли бы вы воспользоваться процедурой? Нравится? blogs.msdn.com/meek/archive/2008/03/26/…
Вы пробовали добавить хранимую процедуру в свою модель сущности?
Да, я сделал. Крейг указал в правильном направлении, поэтому я опубликую POC следующим.
Наконец, с помощью Крейга, вот доказательство концепции. Требуется дополнительное тестирование, но, на первый взгляд, он работает.
Во-первых: я создал две таблицы, одну для данных, другую для регистрации.
-- This is for the data
create table datastuff (
id int not null identity(1, 1),
userid nvarchar(64) not null default(''),
primary key(id)
)
go
-- This is for the log
create table naplo (
id int not null identity(1, 1),
userid nvarchar(64) not null default(''),
datum datetime not null default('2099-12-31'),
primary key(id)
)
go
Во-вторых: создать триггер для вставки.
create trigger myTrigger on datastuff for insert as
declare @User_id int,
@User_context varbinary(128),
@User_id_temp varchar(64)
select @User_context = context_info
from master.dbo.sysprocesses
where spid=@@spid
set @User_id_temp = cast(@User_context as varchar(64))
declare @insuserid nvarchar(64)
select @insuserid=userid from inserted
insert into naplo(userid, datum)
values(@User_id_temp, getdate())
go
Вы также должны создать триггер для обновления, который будет немного сложнее, потому что он должен проверять каждое поле на предмет измененного содержимого.
Таблица журнала и триггер должны быть расширены для хранения таблицы и поля, которые были созданы / изменены, но я надеюсь, что вы уловили идею.
В-третьих: создайте хранимую процедуру, которая подставляет идентификатор пользователя в контекстную информацию SQL.
create procedure userinit(@userid varchar(64))
as
begin
declare @m binary(128)
set @m = cast(@userid as binary(128))
set context_info @m
end
go
Мы готовы со стороной SQL. А вот и часть C#.
Создайте проект и добавьте в проект EDM. EDM должен содержать таблицу данных (или таблицы, которые необходимо отслеживать на предмет изменений) и SP.
Теперь сделайте что-нибудь с объектом сущности (например, добавьте новый объект данных) и подключитесь к событию SavingChanges.
using (testEntities te = new testEntities())
{
// Hook to the event
te.SavingChanges += new EventHandler(te_SavingChanges);
// This is important, because the context info is set inside a connection
te.Connection.Open();
// Add a new datastuff
datastuff ds = new datastuff();
// This is coming from a text box of my test form
ds.userid = textBox1.Text;
te.AddTodatastuff(ds);
// Save the changes
te.SaveChanges(true);
// This is not needed, only to make sure
te.Connection.Close();
}
Внутри SavingChanges мы вводим наш код, чтобы установить контекстную информацию соединения.
// Take my entity
testEntities te = (testEntities)sender;
// Get it's connection
EntityConnection dc = (EntityConnection )te.Connection;
// This is important!
DbConnection storeConnection = dc.StoreConnection;
// Create our command, which will call the userinit SP
DbCommand command = storeConnection.CreateCommand();
command.CommandText = "userinit";
command.CommandType = CommandType.StoredProcedure;
// Put the user id as the parameter
command.Parameters.Add(new SqlParameter("userid", textBox1.Text));
// Execute the command
command.ExecuteNonQuery();
Поэтому перед сохранением изменений мы открываем соединение объекта, внедряем наш код (не закрывайте соединение в этой части!) И сохраняем наши изменения.
И не забывай! Это должно быть расширено для ваших нужд регистрации и должно быть хорошо протестировано, потому что это показывает только возможность!
Закрытие соединения важно из-за System.ArgumentException: EntityConnection can only be constructed with a closed DbConnection.
Нам пришлось решить эту проблему по-другому.
В вашем методе SavingChanges:
Тогда в вашем коде вы должны использовать унаследованный класс.
Спасибо, что указали мне правильное направление. Однако в моем случае мне также нужно установить контекстную информацию при выполнении операторов выбора, потому что я запрашиваю представления, которые используют контекстную информацию для управления безопасностью на уровне строк пользователем.
Мне показалось, что проще всего присоединиться к событию StateChanged соединения и просто наблюдать за изменением состояния с закрытого на открытое. Затем я вызываю процедуру, которая устанавливает контекст, и она работает каждый раз, даже если EF решает сбросить соединение.
private int _contextUserId;
public void SomeMethod()
{
var db = new MyEntities();
db.Connection.StateChange += this.Connection_StateChange;
this._contextUserId = theCurrentUserId;
// whatever else you want to do
}
private void Connection_StateChange(object sender, StateChangeEventArgs e)
{
// only do this when we first open the connection
if (e.OriginalState == ConnectionState.Open ||
e.CurrentState != ConnectionState.Open)
return;
// use the existing open connection to set the context info
var connection = ((EntityConnection) sender).StoreConnection;
var command = connection.CreateCommand();
command.CommandText = "proc_ContextInfoSet";
command.CommandType = CommandType.StoredProcedure;
command.Parameters.Add(new SqlParameter("ContextUserID", this._contextUserId));
command.ExecuteNonQuery();
}
Мне нравится этот подход тем, что он также будет работать, когда хранимые процедуры вызываются из EF. Предположительно, он добавляет досадные дополнительные накладные расходы на каждый выбор, поскольку он добавляет выполнение процедуры туда и обратно. Было бы неплохо иметь способ избежать этого, особенно для людей, которым не нужен набор CONTEXT_INFO для выбора.
Может быть, удастся создать новый DbExecutionStrategy, который будет выполнять эту процедуру в режиме онлайн с любыми другими выполнениями, удалив лишний круговой обход?
@Rory - TBH, этому больше 5 лет, и я даже не помню, чтобы это писал. Ясно, что так и было, но теперь это ушло из моей головы. ;)
вы, наверное, забыли больше, чем многие люди когда-либо знали :)
Просто принудительно выполните SET CONTEXT_INFO, используя свой DbContext или ObjectContext:
...
FileMoverContext context = new FileMoverContext();
context.SetSessionContextInfo(Environment.UserName);
...
context.SaveChanges();
FileMoverContext наследуется от DbContext и имеет метод SetSessionContextInfo. Вот как выглядит мой SetSessionContextInfo (...):
public bool SetSessionContextInfo(string infoValue)
{
try
{
if (infoValue == null)
throw new ArgumentNullException("infoValue");
string rawQuery =
@"DECLARE @temp varbinary(128)
SET @temp = CONVERT(varbinary(128), '";
rawQuery = rawQuery + infoValue + @"');
SET CONTEXT_INFO @temp";
this.Database.ExecuteSqlCommand(rawQuery);
return true;
}
catch (Exception e)
{
return false;
}
}
Теперь вы просто настроили триггер базы данных, который может обращаться к CONTEXT_INFO (), и установить поле базы данных с его помощью.
У меня был похожий сценарий, который я решил, выполнив следующие шаги:
Сначала создайте общий репозиторий для всех операций CRUD, таких как отслеживание, что всегда является хорошим подходом. открытый класс GenericRepository: IGenericRepository, где T: class
Теперь напишите свои действия, такие как «Public virtual void Update (T entityToUpdate)».
Найдите ссылку на полный класс ниже:
public class GenericRepository<T> : IGenericRepository<T> where T : class
{
internal SampleDBContext Context;
internal DbSet<T> DbSet;
/// <summary>
/// Constructor to initialize type collection
/// </summary>
/// <param name = "context"></param>
public GenericRepository(SampleDBContext context)
{
Context = context;
DbSet = context.Set<T>();
}
/// <summary>
/// Get query on current entity
/// </summary>
/// <returns></returns>
public virtual IQueryable<T> GetQuery()
{
return DbSet;
}
/// <summary>
/// Performs read operation on database using db entity
/// </summary>
/// <param name = "filter"></param>
/// <param name = "orderBy"></param>
/// <param name = "includeProperties"></param>
/// <returns></returns>
public virtual IEnumerable<T> Get(Expression<Func<T, bool>> filter = null, Func<IQueryable<T>,
IOrderedQueryable<T>> orderBy = null, string includeProperties = "")
{
IQueryable<T> query = DbSet;
if (filter != null)
{
query = query.Where(filter);
}
query = includeProperties.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Aggregate(query, (current, includeProperty) => current.Include(includeProperty));
if (orderBy == null)
return query.ToList();
else
return orderBy(query).ToList();
}
/// <summary>
/// Performs read by id operation on database using db entity
/// </summary>
/// <param name = "id"></param>
/// <returns></returns>
public virtual T GetById(object id)
{
return DbSet.Find(id);
}
/// <summary>
/// Performs add operation on database using db entity
/// </summary>
/// <param name = "entity"></param>
public virtual void Insert(T entity)
{
//if (!entity.GetType().Name.Contains("AuditLog"))
//{
// LogEntity(entity, "I");
//}
DbSet.Add(entity);
}
/// <summary>
/// Performs delete by id operation on database using db entity
/// </summary>
/// <param name = "id"></param>
public virtual void Delete(object id)
{
T entityToDelete = DbSet.Find(id);
Delete(entityToDelete);
}
/// <summary>
/// Performs delete operation on database using db entity
/// </summary>
/// <param name = "entityToDelete"></param>
public virtual void Delete(T entityToDelete)
{
if (!entityToDelete.GetType().Name.Contains("AuditLog"))
{
LogEntity(entityToDelete, "D");
}
if (Context.Entry(entityToDelete).State == EntityState.Detached)
{
DbSet.Attach(entityToDelete);
}
DbSet.Remove(entityToDelete);
}
/// <summary>
/// Performs update operation on database using db entity
/// </summary>
/// <param name = "entityToUpdate"></param>
public virtual void Update(T entityToUpdate)
{
if (!entityToUpdate.GetType().Name.Contains("AuditLog"))
{
LogEntity(entityToUpdate, "U");
}
DbSet.Attach(entityToUpdate);
Context.Entry(entityToUpdate).State = EntityState.Modified;
}
public void LogEntity(T entity, string action = "")
{
try
{
//*********Populate the audit log entity.**********
var auditLog = new AuditLog();
auditLog.TableName = entity.GetType().Name;
auditLog.Actions = action;
auditLog.NewData = Newtonsoft.Json.JsonConvert.SerializeObject(entity);
auditLog.UpdateDate = DateTime.Now;
foreach (var property in entity.GetType().GetProperties())
{
foreach (var attribute in property.GetCustomAttributes(false))
{
if (attribute.GetType().Name == "KeyAttribute")
{
auditLog.TableIdValue = Convert.ToInt32(property.GetValue(entity));
var entityRepositry = new GenericRepository<T>(Context);
var tempOldData = entityRepositry.GetById(auditLog.TableIdValue);
auditLog.OldData = tempOldData != null ? Newtonsoft.Json.JsonConvert.SerializeObject(tempOldData) : null;
}
if (attribute.GetType().Name == "CustomTrackAttribute")
{
if (property.Name == "BaseLicensingUserId")
{
auditLog.UserId = ValueConversion.ConvertValue(property.GetValue(entity).ToString(), 0);
}
}
}
}
//********Save the log in db.*********
new UnitOfWork(Context, null, false).AuditLogRepository.Insert(auditLog);
}
catch (Exception ex)
{
Logger.LogError(string.Format("Error occured in [{0}] method of [{1}]", Logger.GetCurrentMethod(), this.GetType().Name), ex);
}
}
}
CREATE TABLE [dbo].[AuditLog](
[AuditId] [BIGINT] IDENTITY(1,1) NOT NULL,
[TableName] [nvarchar](250) NULL,
[UserId] [int] NULL,
[Actions] [nvarchar](1) NULL,
[OldData] [text] NULL,
[NewData] [text] NULL,
[TableIdValue] [BIGINT] NULL,
[UpdateDate] [datetime] NULL,
CONSTRAINT [PK_DBAudit] PRIMARY KEY CLUSTERED
(
[AuditId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY =
OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
Это то, что я использовал, нашел здесь Я изменил его, потому что он не работал
private object GetPrimaryKeyValue(DbEntityEntry entry)
{
var objectStateEntry = ((IObjectContextAdapter)this).ObjectContext.ObjectStateManager.GetObjectStateEntry(entry.Entity);
object o = objectStateEntry.EntityKey.EntityKeyValues[0].Value;
return o;
}
private bool inExcludeList(string prop)
{
string[] excludeList = { "props", "to", "exclude" };
return excludeList.Any(s => s.Equals(prop));
}
public int SaveChanges(User user, string UserId)
{
var modifiedEntities = ChangeTracker.Entries()
.Where(p => p.State == EntityState.Modified).ToList();
var now = DateTime.Now;
foreach (var change in modifiedEntities)
{
var entityName = ObjectContext.GetObjectType(change.Entity.GetType()).Name;
var primaryKey = GetPrimaryKeyValue(change);
var DatabaseValues = change.GetDatabaseValues();
foreach (var prop in change.OriginalValues.PropertyNames)
{
if (inExcludeList(prop))
{
continue;
}
string originalValue = DatabaseValues.GetValue<object>(prop)?.ToString();
string currentValue = change.CurrentValues[prop]?.ToString();
if (originalValue != currentValue)
{
ChangeLog log = new ChangeLog()
{
EntityName = entityName,
PrimaryKeyValue = primaryKey.ToString(),
PropertyName = prop,
OldValue = originalValue,
NewValue = currentValue,
ModifiedByName = user.LastName + ", " + user.FirstName,
ModifiedById = UserId,
ModifiedBy = user,
ModifiedDate = DateTime.Now
};
ChangeLogs.Add(log);
}
}
}
return base.SaveChanges();
}
public class ChangeLog
{
public int Id { get; set; }
public string EntityName { get; set; }
public string PropertyName { get; set; }
public string PrimaryKeyValue { get; set; }
public string OldValue { get; set; }
public string NewValue { get; set; }
public string ModifiedByName { get; set; }
[ForeignKey("ModifiedBy")]
[DisplayName("Modified By")]
public string ModifiedById { get; set; }
public virtual User ModifiedBy { get; set; }
[Column(TypeName = "datetime2")]
public DateTime? ModifiedDate { get; set; }
}
Серьезно, пользователь хочет, чтобы вы использовали EF, а затем требует, чтобы вы придерживались SQL 2000?