I'm running with the code below and I can see that one key is created in the table [DataProtectionKeys]
services.AddDataProtection()
.SetApplicationName(dataProtectionSettings.ApplicationName)
.ProtectKeysWithCertificate(serviceCertificate)
.UnprotectKeysWithAnyCertificate(serviceCertificate)
.PersistKeysToDbContext<DataProtectionContext>();
I'm using this database context (am I missing something?):
class DataProtectionContext : DbContext, IDataProtectionKeyContext
{
/// <summary>
/// A recommended constructor overload when using EF Core with dependency injection.
/// </summary>
/// <param name="options"></param>
public DataProtectionContext(DbContextOptions<DataProtectionContext> options)
: base(options) { }
/// <summary>
/// This maps to the table that stores keys.
/// </summary>
public DbSet<DataProtectionKey> DataProtectionKeys { get; set; }
}
But, if I change to ApplicationName string value to something else, I don't see that a new key is created
Any idea why or how to fix it so this database table can support multiple application
I have an inheritance hierarchy setup that I am mapping to a DB via TPT in Code first. For the most part the hierarchy is one level deep, but sometimes it it two. My base class looks like this:
public class AuditEvent
{
public int AuditEventID;
//other stuff
};
Then I have a bunch of other classes that look like this (with different names and properties):
public class PageRequest : AuditEvent
{
/// <summary>
/// Page Request Id (Primary Key)
/// </summary>
public Int64 PageRequestID { get; set; }
/// <summary>
/// Screen (page) being requested
/// </summary>
public string Screen { get; set; }
/// <summary>
/// Http Method
/// </summary>
public string HttpMethod { get; set; }
/// <summary>
/// Confirmation Logs linked to this page request
/// </summary>
public virtual List<ConfirmationLog> ConfirmationLogs { get; set; }
}
This specific class (PageRequest) is a parent to one other class called ConfirmationLog, which looks like this:
/// <summary>
/// Object used to log confirmations to the auditing database
/// </summary>
public class ConfirmationLog : PageRequest
{
/// <summary>
/// Confirmation ID
/// </summary>
public long ConfirmationID { get; set; }
/// <summary>
/// Confirmation number
/// </summary>
public string ConfirmationNum { get; set; }
/// <summary>
/// Web action ID (automated alert or transaciton confirmation number)
/// </summary>
public int WebActionID { get; set; }
}
I'm configuring the mappings using configuration classes and the fluent API, like so:
/// <summary>
/// Configuration class for PageRequest
/// </summary>
public class PageRequestConfiguration : EntityTypeConfiguration<PageRequest>
{
/// <summary>
/// Default constructor
/// </summary>
public PageRequestConfiguration()
{
//Table
ToTable("PageRequests");
//primary key
HasKey(a => a.PageRequestID);
//Properties
Property(a => a.PageRequestID).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
Property(a => a.Screen).IsRequired().HasMaxLength(100);
Property(a => a.HttpMethod).IsRequired().HasMaxLength(10);
}
}
/// <summary>
/// Confirmation Log configuration class. Configures the confirmation log class for the db model
/// </summary>
public class ConfirmationLogConfiguration : EntityTypeConfiguration<ConfirmationLog>
{
/// <summary>
/// Default constructor
/// </summary>
public ConfirmationLogConfiguration()
{
//Map to Table
ToTable("ConfirmationLogs");
//Primary Key
HasKey(a => a.ConfirmationID);
//required fields
Property(a => a.ConfirmationID).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
Property(a => a.PageRequestID).IsRequired();
Property(a => a.ConfirmationNum).IsRequired().HasMaxLength(12);
Property(a => a.WebActionID).IsRequired();
}
}
I then create a rather large LINQ query based on this hierarchy. I'll spare that query because it's composed in about 10 steps, and I don't think that's the source of my problem. The problem is, when I run the query, the SQL generated for some reason thinks that the column AuditEventID (the primary key for the base class), exists on the ConfirmationLogs table (the grandchild table). The ConfirmationLogs table has a foreign key to it's parent table (PageRequests), which then has the foreign key to it's parent table (AuditEvents).
My question is, did I set this hierarchy up wrong? Does the "grandchild" table need the foreign key to both it's parent and grandparent for this to function? (if it does I find that unfortunate).
I'm positive that the inheritance relationship is throwing things off because if I don't make ConfirmationLogs a child of PageRequests and configure the relationship to PageRequests with HasRequired()/WithMany(), things work fine.
Any help would be appreciated.
Update
So, after further investigation I think there is a general problem with the way I'm trying to use inheritance. I should note that I'm trying to map code first to an existing database. In the database, I have my AuditEvent table, and a bunch of "child" tables like PageRequest. Page request has it's own primary key called PageRequestID, as well as a foreign key called AuditEventID. The other child tables are setup the same way. In my Configuration class for PageRequest (listed above), I'm trying to map this by using the HasKey function to say that the PageRequestID is the primary key, and assuming that EF will know about the foreign key AuditEventID by convention and inheritance. I should also note that I can write to the DB using the model just fine. If I want to write a PageRequest, I create PageRequest object, populate all the required fields as defined by both the PageRequest and AuditEvent base class, and save through the context. EF creates the AuditEvent record, and the pageRequest record with the FK back to AuditEvent.
What makes me think I'm not using inheritance right is that I allowed EF to create my database for me, using the model and mapping I've created. For the PageRequest table (and all other child tables), EF actually created a primary key called AuditEventID (even though my configuration is telling it to do otherwise). This key is also labeled as a foreign key, and the column that I want to create as a primary key (PageRequestID in this example) is just configured as being required (non-nullable). So it appears that EF taking the primary key from my BASE class and using that as a primary key AND foreign key in my child classes, almost like the concept of the AuditEventID is spread between the parent and child tables. Is there a way to change this behavior?
You are saying this didn't work, and it still expected an AuditRequestID in the table that had the ConfirmationLog object? I'm looking at the reference: Specifying Not to Map a CLR Property to a Column in the Database in http://msdn.microsoft.com/en-us/data/jj591617#1.6
public ConfirmationLogConfiguration()
{
//Map to Table
ToTable("ConfirmationLogs");
//Primary Key
HasKey(a => a.ConfirmationID);
//required fields
Property(a => a.ConfirmationID).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
Property(a => a.PageRequestID).IsRequired();
Property(a => a.ConfirmationNum).IsRequired().HasMaxLength(12);
Property(a => a.WebActionID).IsRequired();
Ignore(a => a.AuditEventID);
}
Good luck.
I'm using EF4.1 for DAL for an application, and I use DbContext template generator with POCP entities. The model is created from the database, so all the fields / PK's / FK's / relations are already defined in database.
I need to find out in code which are the fields for the table for an entity.
Some tables might have a single field PK, while other might have compound PK. Whan I need is to have a method that will return me a List for an entity, with all field names composing the primary keys. It can be from DbContext, or from entity, doesn't matter.
I could even customize the template to generate a method in POCO entity, as below:
public List<string> PrimaryKey()
{
List<string> pk = new List<string>();
pk.AddRange(
new string[] {"Field1", "Field2"});
return pk;
}
but I don't know how to find the field names composing the PK.
Any suggestions?
Thank you
I did some research and I modified the template to generate a property that returns this for me.
First I customized the template to generate strong typed names for field names (I hate using strings in code which can cause problems when refactoring). Then that is used to generate a property that returns primary key fields as List
Here are the changes to template (I used ADO.NET DbContext Template Generator, but for any other template it should be very similar):
<#=Accessibility.ForType(entity)#>
<#=code.SpaceAfter(code.AbstractOption(entity))#>partial class <#=code.Escape(entity)#>
<#=code.StringBefore(" : ", code.Escape(entity.BaseType))#>
{
<#
WriteStrongTypedPropertyNames(code, entity); // <-- Insert this
WritePrimaryKeyProperty(code, entity); // <-- and this
// .....
And at the end of template file add:
<#+
void WriteStrongTypedPropertyNames(CodeGenerationTools code, EntityType entity)
{
#> /// <summary>
/// Strong typed property names
/// </summary>
public class PropertyNames
{
<#+
foreach (var property in entity.Properties)
{
#>
public const string <#=code.Escape(property)#> = "<#=property#>";
<#+
}
#>
}
<#+
}
void WritePrimaryKeyProperty(CodeGenerationTools code, EntityType entity)
{
#> /// <summary>
/// Returns primary key as List
/// </summary>
public List<string> PrimaryKey
{
get
{
List<string> pk = new List<string>();
pk.AddRange(
new string[] {
<#+
foreach (var member in entity.KeyMembers)
{
string delim = "";
#>
<#=delim#> PropertyNames.<#=code.Escape(member.Name)#>
<#+
delim=",";
}
#> });
return pk;
}
}
<#+
}
#>
It generates a code as below in the entity:
/// <summary>
/// Strong typed property names
/// </summary>
public class PropertyNames
{
public const string AppID = "AppID";
public const string AppName = "AppName";
public const string AppCode = "AppCode";
}
/// <summary>
/// Returns primary key as List
/// </summary>
public List<string> PrimaryKey
{
get
{
List<string> pk = new List<string>();
pk.AddRange(
new string[] {
PropertyNames.AppID
});
return pk;
}
}
Hope this helps someone
I had a tough time trying to do almost the same thing, getting the primary key name and value at runtime when the type is unknown, from a DbContext. I was just get trying to implement an auditing scheme for deletes, and every solution i find involves superfluous code that I dont really understand. The EntityKey is not available from a DbContext, which is also confusing and annoying. The last 5 lines may save you 5 hours and 1 yr of baldness. I am not attempting this for Inserts, so if you do, you need to inspect those values carefully as they may be 0 or null.
foreach(var entry in ChangeTracker.Entries<IAuditable>()) {
...
case EntityState.Deleted:
...
var oc = ((IObjectContextAdapter.this).ObjectContext; //.this is a DbContext EntityKey
ek = oc.ObjectStateManager.GetObjectStateEntry(entry.Entity).EntityKey;
var tablename= ek.EntitySetName;
var primaryKeyField = ek.EntityKeyValues[0].Key; //assumes only 1 primary key var
primaryKeyValue = ek.EntityKeyValues[0].Value;
stumbled across this post after having done something similar to #bzamfir. I thought I would post what I did in hopes that it might alleviate some of the headaches that #Bill mentions that I have also experienced!
Using the DBContext, I also modified the Template.
1) Add Imports System.ComponentModel.DataAnnotations to the list of Imports already in the file
2) Add code to the Primitive Properties For Each Loop which adds the KeyAttribute to the primary key properties. It should look like:
For Each edmProperty As EdmProperty In primitiveProperties
If ef.IsKey(edmProperty) Then
#><#= code.StringBefore("<KeyAttribute()>",Environment.NewLine & CodeRegion.GetIndent(region.CurrentIndentLevel + 2))#><#
End If
WriteProperty(code, edmProperty)
Next
You can add this Extension Method to your code somewhere which will find the Primary Key based off of the KeyAttribute.
<Extension()>
Public Function FindPrimaryKeyProperty(Of T As Class)(context As DbContext, TEntity As T) As PropertyInfo
Return TEntity.GetType().GetProperties().Single(Function(p) p.GetCustomAttributes(GetType(KeyAttribute), True).Count > 0)
End Function
I do recognize the point that this function will fail if you have more than one property flagged with KeyAttribute, however, for my situation this was not the case.
Alternatively, I just came across this solution that appears to work just fine and doesn't require any Template editing (+1).
http://blog.oneunicorn.com/2012/05/03/the-key-to-addorupdate/
public static IEnumerable<string> KeysFor(this DbContext context, Type entityType)
{
Contract.Requires(context != null);
Contract.Requires(entityType != null);
entityType = ObjectContext.GetObjectType(entityType);
var metadataWorkspace =
((IObjectContextAdapter)context).ObjectContext.MetadataWorkspace;
var objectItemCollection =
(ObjectItemCollection)metadataWorkspace.GetItemCollection(DataSpace.OSpace);
var ospaceType = metadataWorkspace
.GetItems<EntityType>(DataSpace.OSpace)
.SingleOrDefault(t => objectItemCollection.GetClrType(t) == entityType);
if (ospaceType == null)
{
throw new ArgumentException(
string.Format(
"The type '{0}' is not mapped as an entity type.",
entityType.Name),
"entityType");
}
return ospaceType.KeyMembers.Select(k => k.Name);
}
I've the following table definition in MSSQL:
CREATE TABLE [User] (
[Id] bigint identity(1,1) NOT NULL,
[Email] nvarchar(256),
[PasswordHash] nvarchar(128) NOT NULL,
[PasswordFormat] int DEFAULT ((0)) NOT NULL,
[PasswordSalt] nvarchar(10) NOT NULL,
[Timestamp] timestamp
)
;
The EDMX property for Timestamp looks like this: (Note only the red property has been manually changed by me)
I used the t4 template to automatically generate POCO entities.
The User entity looks like this:
public partial class User : IEntity
{
public virtual long Id
{
get;
set;
}
...
[TimestampAttribute]
[ConcurrencyCheck]
[SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Justification = "Autogenerated by T4.")]
public virtual byte[] Timestamp
{
get;
set;
}
...
}
When doing a 'SaveChanges' operation on the ObjectContext, I get a validation error for the User entity which is called : The Timestamp field is required
Solution:
I've changed the T4 generated User class to: (removed the 'ConcurrencyCheck' attribute)
public partial class User : IEntity
{
public virtual long Id
{
get;
set;
}
...
[TimestampAttribute]
[SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Justification = "Autogenerated by T4.")]
public virtual byte[] Timestamp
{
get;
set;
}
...
}
And I've added a generic metadata class which is used by all Entities which excludes the Timestamp property :
/// <summary>
/// A MetaData which defines some default metadata for an Entity
/// </summary>
public class EntityMetaData
{
/// <summary>
/// Initializes a new instance of the <see cref="EntityMetaData"/> class.
/// </summary>
protected EntityMetaData()
{
}
/// <summary>
/// Gets or sets the timestamp.
/// Note : this field is excluded on the client.
/// </summary>
/// <value>The timestamp.</value>
[Exclude]
public byte[] Timestamp { get; set; }
}
This solves the issue.
One option is to set Nullable attribute to true in EDMX model but keep NOT NULL constraint in the database.
As generated type for Timestamp (RowVersion) is reference type (byte[]) and thus can accept null value, it should not break any existing code.
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 the database, but we need to log the "real" user id.
We can do this in t-sql by writing triggers for every table insert and update, and using context_info to store the user id. We passed the user id to a stored procedure, stored the user id in the contextinfo, and the trigger could use this info to write log rows to the log table.
I can not find the place or way where or how can I do something similar using EF. So the main goal is: if I make a change in the data via EF, I would like to log the exact data change to a table in a semi-automatic way (so I don't want to check for every field for change before saving the object). We are using EntitySQL.
Unfortunately we have to stick on SQL 2000 so the data change capture introduced in SQL2008 is not an option (but maybe that's also not the right way for us).
Any ideas, links or starting points?
[Edit]
Some notes: by using ObjectContext.SavingChanges eventhandler, I can get the point where I can inject the SQL statement to initialize the contextinfo. However I cannot mix the EF and the standard SQL. So I can get the EntityConnection but I cannot execute a T-SQL statement using it. Or I can get the connection string of the EntityConnection and create an SqlConnection based on it, but it will be a different connection, so the contextinfo will not affect the save made by the EF.
I tried the following in the SavingChanges handler:
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();
Error: The value of EntityCommand.CommandText is not valid for a StoredProcedure command.
The same with SqlParameter instead of EntityParameter: SqlParameter cannot be used.
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();
Error: The query syntax is not valid.
So here I am, stuck to create a bridge between Entity Framework and ADO.NET.
If I can get it working, I will post a proof of concept.
How about handling Context.SavingChanges?
Thanks for pointing me in the right direction. However, in my case, I also need to set the context info when doing select statements, because I am querying views that use the context info to control row-level security by user.
I found it easiest to attach to the StateChanged event of the connection and just watch for the change from not-open to open. Then I call the proc that sets context and it works every time, even if EF decides to reset the connection.
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();
}
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!
Have you tried adding the stored procedure to your entity model?
Simply force an execution of the SET CONTEXT_INFO by using your DbContext or ObjectContext:
...
FileMoverContext context = new FileMoverContext();
context.SetSessionContextInfo(Environment.UserName);
...
context.SaveChanges();
FileMoverContext inherits from DbContext and has a SetSessionContextInfo method.
Here is what my SetSessionContextInfo(...) looks like:
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;
}
}
Now you just set up a database trigger which can access the CONTEXT_INFO() and set a database field using it.
We had solve this problem in a different way.
Inherit a class from your generated entity container class
Make the base entity class abstract. You can do it by a partial class definition in a separate file
In the inherited class hide the SavingChanges method with your own, using the new keyword in the method definition
In your SavingChanges method:
a, open an entity connection
execute the user context stored procedure with ebtityclient
call base.SaveChanges()
close the entityconnection
In your code you have to use the inherited class then.
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)".
Wherever you required logging / Auditing; just call a user defined function as follows "LogEntity(entityToUpdate, "U");".
Refer below pasted file/class to define "LogEntity" function. In this function, in case of update and delete we would get the old entity through primary key to insert in audit table. To identify primary key and get its value I used reflection.
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]
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; }
}