There is a need from a customer to log every data change to a logging table with the actual user who made the modification. The application is using one SQL user to access t
Have you tried adding the stored procedure to your entity model?
How about handling Context.SavingChanges?
This is what I used found here I modified it because it didn't work
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; }
}
I had somewhat similar scenario, which I resolved through following steps:
First create a generic repository for all CRUD operations like following, which is always a good approach. public class GenericRepository : IGenericRepository where T : class
Now write your actions like "public virtual void Update(T entityToUpdate)".
Find reference of complete class below:
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]
Finally with Craig's help, here is a proof of concept. It needs more testing, but for first look it is working.
First: I created two tables, one for data one for logging.
-- 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
Second: create a trigger for insert.
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
You should also create a trigger for update, which will be a little bit more sophisticated, because it needs to check every field for changed content.
The log table and the trigger should be extended to store the table and field which is created/changed, but I hope you got the idea.
Third: create a stored procedure which fills in the user id to the SQL context info.
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
We are ready with the SQL side. Here comes the C# part.
Create a project and add an EDM to the project. The EDM should contain the datastuff table (or the tables you need to watch for changes) and the SP.
Now do something with the entity object (for example add a new datastuff object) and hook to the SavingChanges event.
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();
}
Inside the SavingChanges we inject our code to set the context info of the connection.
// 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();
So before saving the changes, we open the object's connection, inject our code (don't close the connection in this part!) and save our changes.
And don't forget! This needs to be extended for your logging needs, and needs to be well tested, because this show only the possibility!
We had solve this problem in a different way.
In your SavingChanges method:
In your code you have to use the inherited class then.