Подтвердить что ты не робот

Регистрация каждого изменения данных с помощью Entity Framework

Необходимо, чтобы клиент регистрировал каждое изменение данных в таблице протоколирования с фактическим пользователем, внесшим изменения. Приложение использует один пользователь 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. Если я смогу заставить его работать, я опубликую доказательство концепции.

4b9b3361

Ответ 1

Как обрабатывать контекст. SavingChanges?

Ответ 2

Спасибо, что указал мне в правильном направлении. Однако в моем случае мне также нужно установить контекстную информацию при выполнении операторов 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();
}

Ответ 3

Наконец, с помощью Крейга, это доказательство концепции. Он нуждается в большем количестве тестирования, но для первого взгляда он работает.

Сначала: я создал две таблицы, одну для данных для ведения журнала.

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

Итак, прежде чем сохранять изменения, мы открываем соединение с объектами, вводим наш код (не закрываем соединение в этой части!) и сохраняем наши изменения.

И не забывайте! Это необходимо расширить для ваших потребностей в регистрации и должно быть хорошо протестировано, потому что это показывает только возможность!

Ответ 4

Вы пытались добавить хранимую процедуру к своей модели сущности?

Ответ 5

Мы решили эту проблему по-другому.

  • Наследовать класс из созданного класса контейнера объектов
  • Сделать базовый класс объекта абстрактным. Вы можете сделать это путем определения частичного класса в отдельном файле
  • В унаследованном классе скрыть метод SavingChanges с вашим собственным, используя новое ключевое слово в определении метода
  • В вашем методе SavingChanges: a, откройте соединение с сущностью b, выполните хранимую процедуру контекста пользователя с помощью ebtityclient c, запустите base.SaveChanges() d, закройте entityconnection

В коде вы должны использовать унаследованный класс.

Ответ 6

Просто принудительно выполните 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() и установить поле базы данных, используя его.

Ответ 7

У меня был несколько схожий сценарий, который я решил с помощью следующих шагов:

  • Сначала создайте общий репозиторий для всех операций CRUD, таких как следующие, что всегда является хорошим подходом. public class GenericRepository: IGenericRepository, где T: class

  • Теперь напишите ваши действия как "public virtual void Update (T entityToUpdate)".

  • Где бы вы ни требовали регистрации/аудита; просто вызовите функцию, определенную пользователем, следующим образом: "LogEntity (entityToUpdate," U ");".
  • См. ниже вложенный файл/класс для определения функции "LogEntity". В этой функции в случае обновления и удаления мы должны получить старую сущность через первичный ключ для вставки в таблицу аудита. Чтобы определить первичный ключ и получить его значение, я использовал отражение.

Найти ссылку полного класса ниже:

 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]