Ответ 1
Как обрабатывать контекст. SavingChanges?
Необходимо, чтобы клиент регистрировал каждое изменение данных в таблице протоколирования с фактическим пользователем, внесшим изменения. Приложение использует один пользователь SQL для доступа к базе данных, но нам нужно зарегистрировать "настоящий" идентификатор пользователя.
Мы можем сделать это в t-sql, написав триггеры для каждой вставки и обновления таблицы и используя context_info для хранения идентификатора пользователя. Мы передали идентификатор пользователя в хранимую процедуру, сохранили идентификатор пользователя в контекстеinfo, и триггер мог использовать эту информацию для записи строк журнала в таблицу журналов.
Я не могу найти место или способ, где и как я могу сделать что-то подобное с помощью 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. Если я смогу заставить его работать, я опубликую доказательство концепции.
Как обрабатывать контекст. SavingChanges?
Спасибо, что указал мне в правильном направлении. Однако в моем случае мне также нужно установить контекстную информацию при выполнении операторов select, потому что я запрашиваю представления, которые используют контекстную информацию для управления безопасностью на уровне строк пользователем.
Мне было проще подключиться к событию StateChanged и просто следить за тем, чтобы изменения не были открыты для открытия. Затем я вызываю proc, который устанавливает контекст, и он работает каждый раз, даже если EF решает reset соединение.
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();
}
Наконец, с помощью Крейга, это доказательство концепции. Он нуждается в большем количестве тестирования, но для первого взгляда он работает.
Сначала: я создал две таблицы, одну для данных для ведения журнала.
-- 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 [email protected]@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. Вот часть С#.
Создайте проект и добавьте EDM в проект. EDM должен содержать таблицу данных (или таблицы, которые необходимо отслеживать для изменений) и SP.
Теперь сделайте что-нибудь с объектом сущности (например, добавьте новый объект datastuff) и подключитесь к событию 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 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();
Итак, прежде чем сохранять изменения, мы открываем соединение с объектами, вводим наш код (не закрываем соединение в этой части!) и сохраняем наши изменения.
И не забывайте! Это необходимо расширить для ваших потребностей в регистрации и должно быть хорошо протестировано, потому что это показывает только возможность!
Вы пытались добавить хранимую процедуру к своей модели сущности?
Мы решили эту проблему по-другому.
В коде вы должны использовать унаследованный класс.
Просто принудительно выполните 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, таких как следующие, что всегда является хорошим подходом. public class 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]