I am porting an old project over to ASP.NET 5 and Entity Framework 7. I have used the database first approach (DNX scaffold) to create the model.
The old project is based on Entity Framework 4 and audit tracking is implemented by overriding the SaveChanges method of the DbContext:
public override int SaveChanges(System.Data.Objects.SaveOptions options)
{
int? UserId = null;
if (System.Web.HttpContext.Current != null)
UserId = (from user in Users.Where(u => u.UserName == System.Web.HttpContext.Current.User.Identity.Name) select user.Id).SingleOrDefault();
foreach (ObjectStateEntry entry in ObjectStateManager.GetObjectStateEntries(EntityState.Added | EntityState.Modified))
{
Type EntityType = entry.Entity.GetType();
PropertyInfo pCreated = EntityType.GetProperty("Created");
PropertyInfo pCreatedById = EntityType.GetProperty("CreatedById");
PropertyInfo pModified = EntityType.GetProperty("Modified");
PropertyInfo pModifiedById = EntityType.GetProperty("ModifiedById");
if (entry.State == EntityState.Added)
{
if (pCreated != null)
pCreated.SetValue(entry.Entity, DateTime.Now, new object[0]);
if (pCreatedById != null && UserId != null)
pCreatedById.SetValue(entry.Entity, UserId, new object[0]);
}
if (pModified != null)
pModified.SetValue(entry.Entity, DateTime.Now, new object[0]);
if (pModifiedById != null && UserId != null)
pModifiedById.SetValue(entry.Entity, UserId, new object[0]);
}
}
return base.SaveChanges(options);
}
My question is, how can I implement this in Entity Framework 7? Do I have to take the code first approach?
Basically you have two ways to achieve this:
Using ChangeTracker API (EF 6+):
This is the way we currently do it in EF 6 and it is still valid and working for EF 7:
First you have to make sure your entities are implementing a common interface for audit fields:
public interface IAuditableEntity
{
int? CreatedById { get; set; }
DateTime Created { get; set; }
int? ModifiedById { get; set; }
DateTime Modified { get; set; }
}
Then you can override SaveChanges and update each common field with audit values:
public override int SaveChanges()
{
int? userId = null;
if (System.Web.HttpContext.Current != null)
userId = (from user in Users.Where(u => u.UserName == System.Web.HttpContext.Current.User.Identity.Name) select user.Id).SingleOrDefault();
var modifiedEntries = ChangeTracker.Entries<IAuditableEntity>()
.Where(e => e.State == EntityState.Added || e.State == EntityState.Modified);
foreach (EntityEntry<IAuditableEntity> entry in modifiedEntries)
{
entry.Entity.ModifiedById = UserId;
entry.Entity.Modified = DateTime.Now;
if (entry.State == EntityState.Added)
{
entry.Entity.CreatedById = UserId;
entry.Entity.Created = DateTime.Now;
}
}
return base.SaveChanges();
}
Using EF 7 new "Shadow Properties" Feature:
Shadow properties are properties that do not exist in your entity class. The value and state of these properties is maintained purely in the Change Tracker.
In other words, the audit columns will not be exposed on your entities which seems to be a better option compare to the one above where you have to include them on your entities.
To implement shadow properties, first you have to configure them on your entities. Let's say for example you have a User object that needs to have some audit columns:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<User>().Property<int>("CreatedById");
modelBuilder.Entity<User>().Property<DateTime>("Created");
modelBuilder.Entity<User>().Property<int>("ModifiedById");
modelBuilder.Entity<User>().Property<DateTime>("Modified");
}
Once configured, now you can access them on SaveChanges() override and update their values accordingly:
public override int SaveChanges()
{
int? userId = null;
if (System.Web.HttpContext.Current != null)
userId = (from user in Users.Where(u => u.UserName == System.Web.HttpContext.Current.User.Identity.Name) select user.Id).SingleOrDefault();
var modifiedBidEntries = ChangeTracker.Entries<User>()
.Where(e => e.State == EntityState.Added || e.State == EntityState.Modified);
foreach (EntityEntry<User> entry in modifiedBidEntries)
{
entry.Property("Modified").CurrentValue = DateTime.UtcNow;
entry.Property("ModifiedById").CurrentValue = userId;
if (entry.State == EntityState.Added)
{
entry.Property("Created").CurrentValue = DateTime.UtcNow;
entry.Property("CreatedById").CurrentValue = userId;
}
}
return base.SaveChanges();
}
Final Thoughts:
For implementing something like audit columns, I'll take the Shadow Properties approach since these are cross cutting concerns and do not necessarily belong to my domain objects so having them implemented this way will keep my domain objects nice and clean.
I've worked on a library that might help.
Take a look at Audit.EntityFramework library, it intercepts SaveChanges() and is compatible with EF Core versions.
Related
I'm trying to validate an entity coming from an External context has not changed.
public class Employee
{
public int Id { get; set; }
public string Name { get; set; }
}
I have a method which takes in an entity which has not been loaded from the context.
public bool Validate(Employee employee)
{
using (var context = new Context())
{
return context.Entry(employee).State == EntityState.Modified;
}
}
I would like to attach and verify that the attached entity is not modified from whats in the database.
I would prefer not to manually have to iterate of the properties. Is there a way to hack around this?
No need to attach the external entity. You can use the external entity to set values of the database entity and then check the state of the latter:
public bool Validate(Employee externalEmployee)
{
using var context = new Context(); // C# 8.0
var dbEntity = context.Where(x => x.Id == externalEmployee.Id).SingleOrDefault();
if (dbEntity != null)
{
context.Entry(dbEntity).CurrentValues.SetValues(externalEmployee);
return context.Entry(dbEntity).State == EntityState.Modified;
}
return false; // Or true, depending on your semantics.
}
You can try:
public static List<string> GetChanges<T>(this T obj, T dbObj)
{
List<string> result = new List<string>();
var type = typeof(T);
foreach (var prop in type.GetProperties())
{
var newValue = prop.GetValue(obj, null);
var dbValue = prop.GetValue(dbObj, null);
if(newValue == null && dbValue != null)
{
result.Add(prop.Name);
continue;
}
if (newValue != null && dbValue == null)
{
result.Add(prop.Name);
continue;
}
if (newValue == null && dbValue == null)
continue;
if (!newValue.ToString().Equals(dbValue.ToString()))
result.Add(prop.Name);
}
return result;
}
if resultList.Count > 0, your object has changes.
In your Validate Method:
public bool Validate(Employee employee)
{
using (var context = new Context())
{
Employee dbEmployee = context.Employee.Find(employee.Id);
if(employee.GetChanges(dbEmployee).Count > 0)
return true;
return false;
}
}
It's a god workaround =D
Works for me!
I have Net core API configured with .net core 2.0 and EF core 2.0. it contains repository pattern architecture.
Now, I am trying to implement Audit log for each save change using EF change tracker.
My Issue : Whenever I tries to add a log for edit/modification endpoint, the original value and current value remain same and it's newly updated value. so in that way I am not able to track the modification or a change.
Here is my ApplicationContext file where I have overridden save call.
public class ApplicationContext : DbContext
{
public ApplicationContext(DbContextOptions options) : base(options: options) { }
public DbSet<Item> Item { get; set; }
public DbSet<ChangeLog> ChangeLog { get; set; }
public override int SaveChanges()
{
var modifiedEntities = ChangeTracker.Entries();
foreach (var change in modifiedEntities)
{
var entityType = change.Entity.GetType().Name;
if (entityType == "LogItem")
continue;
if (change.State == EntityState.Modified)
{
foreach (var prop in change.OriginalValues.Properties)
{
var id = change.CurrentValues["Id"].ToString();
//here both originalValue and currentValue are same and it's newly updated value
var originalValue = change.OriginalValues[prop]?.ToString();
var currentValue = change.CurrentValues[prop]?.ToString();
if (originalValue != currentValue)
{
ChangeLog.Add(
new ChangeLog()
{
CreationDateTime = DateTime.Now,
CreationUserId = 1,
Log = $"Edited item named {prop.Name} in {entityType} Id {id}.",
OldValue = originalValue,
NewValue = currentValue,
TableName = entityType,
FieldName = prop.Name
}
);
}
}
}
}
return base.SaveChanges();
}
}
Here is my base repository.
public class EntityBaseRepository<T> : IEntityBaseRepository<T> where T : class, IFullAuditedEntity, new()
{
private readonly ApplicationContext context;
public EntityBaseRepository(ApplicationContext context)
{
this.context = context;
}
public virtual T GetSingle(int id) => context.Set<T>().AsNoTracking().FirstOrDefault(x => x.Id == id);
public virtual T Add(T entity) => Operations(entity: entity, state: EntityState.Added);
public virtual T Update(T entity) => Operations(entity: entity, state: EntityState.Modified);
public virtual T Delete(T entity) => Operations(entity: entity, state: EntityState.Deleted);
public virtual T Operations(T entity, EntityState state)
{
EntityEntry dbEntityEntry = context.Entry<T>(entity);
if (state == EntityState.Added)
{
entity.CreationDateTime = DateTime.UtcNow;
entity.CreationUserId = 1;
context.Set<T>().Add(entity);
dbEntityEntry.State = EntityState.Added;
}
else if (state == EntityState.Modified)
{
entity.LastModificationDateTime = DateTime.UtcNow;
entity.LastModificationUserId = 1;
//var local = context.Set<T>().Local.FirstOrDefault(entry => entry.Id.Equals(entity.Id));
//if (local != null)
//{
// context.Entry(local).State = EntityState.Detached;
//}
dbEntityEntry.State = EntityState.Modified;
}
else if (state == EntityState.Deleted)
{
entity.DeletionFlag = true;
entity.DeletionUserId = 1;
entity.DeletionDateTime = DateTime.UtcNow;
dbEntityEntry.State = EntityState.Modified;
}
return entity;
}
public virtual void Commit() => context.SaveChanges();
}
And Lastly my controller with end point for put.
[Produces("application/json")]
[Route("api/Item")]
public class ItemController : Controller
{
private readonly IItemRepository repository;
private readonly IChangeLogRepository changeLogRepository;
private readonly IMapper mapper;
public ItemController(IItemRepository repository, IChangeLogRepository _changeLogRepository, IMapper mapper)
{
this.repository = repository;
this.changeLogRepository = _changeLogRepository;
this.mapper = mapper;
}
[HttpPut]
public IActionResult Put([FromBody]ItemDto transactionItemDto)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
if (transactionItemDto.Id <= 0)
{
return new NotFoundResult();
}
Item item = repository.GetSingle(transactionItemDto.Id); //find entity first
if (item == null)
{
return new NotFoundResult();
}
//map all the properties and commit
var entity = mapper.Map<Item>(transactionItemDto);
var updatedItem = repository.Update(entity);
repository.Commit();
return new OkObjectResult(mapper.Map<Item, ItemDto>(source: updatedItem));
}
}
I am not sure where I am doing any mistake, I tried to check this case in SO, but no luck.
any help will be appreciated, thanks.
I am not using the repository pattern, but I have implemented a very similar audit log for EF Core 2.1. I loop through the list of changes that are being tracked by the entity framework change tracker, and log them.
What I have noticed is that when I want to update an entity, there are two ways to do it. One is that I read the existing entity from the database, reassign the variable, then save it. The second way is to simply create an object, attach it to the database context, and set the property I want to update to the modified state. When I do this, my auditing won't work for the original value, since the original value was never actually read from the database.
examples:
//auditing works fine
var myEntity = await db.MyEntity.FindAsync(entityId);
myEntity.Property = newValue;
await db.SaveChangesAsync();
//auditing can't track the old value
var myEntity = new MyEntity();
db.Attach(myEntity);
myEntity.Property = newValue;
await db.SaveChangesAsync();
Here is the important bit of my auditing code for example
foreach (var entity in db.ChangeTracker.Entries())
{
if(entity.State == EntityState.Detached || entity.State == EntityState.Unchanged)
{
continue;
}
var audits = new List<Audit>();
//the typeId is a string representing the primary keys of this entity.
//this will not be available for ADDED entities with generated primary keys, so we need to update those later
string typeId;
if (entity.State == EntityState.Added && entity.Properties.Any(prop => prop.Metadata.IsPrimaryKey() && prop.IsTemporary))
{
typeId = null;
}
else
{
var primaryKey = entity.Metadata.FindPrimaryKey();
typeId = string.Join(',', primaryKey.Properties.Select(prop => prop.PropertyInfo.GetValue(entity.Entity)));
}
//record an audit for each property of each entity that has been changed
foreach (var prop in entity.Properties)
{
//don't audit anything about primary keys (those can't change, and are already in the typeId)
if(prop.Metadata.IsPrimaryKey() && entity.Properties.Any(p => !p.Metadata.IsPrimaryKey()))
{
continue;
}
//ignore values that won't actually be written
if(entity.State != EntityState.Deleted && entity.State != EntityState.Added && prop.Metadata.AfterSaveBehavior != PropertySaveBehavior.Save)
{
continue;
}
//ignore values that won't actually be written
if (entity.State == EntityState.Added && prop.Metadata.BeforeSaveBehavior != PropertySaveBehavior.Save)
{
continue;
}
//ignore properties that didn't change
if(entity.State == EntityState.Modified && !prop.IsModified)
{
continue;
}
var audit = new Audit
{
Action = (int)entity.State,
TypeId = typeId,
ColumnName = prop.Metadata.SqlServer().ColumnName,
OldValue = (entity.State == EntityState.Added || entity.OriginalValues == null) ? null : JsonConvert.SerializeObject(prop.OriginalValue),
NewValue = entity.State == EntityState.Deleted ? null : JsonConvert.SerializeObject(prop.CurrentValue)
};
}
//Do something with audits
}
I think I see the issue with your code. In your controller:
//map all the properties and commit
var entity = mapper.Map<Item>(transactionItemDto);
var updatedItem = repository.Update(entity);
repository.Commit();
In that code you are taking your DTO and mapping it to a new instance of Item. That new instance of Item knows nothing of the current database values, that is why you are seeing the same new values for both OriginalValue and CurrentValue.
If you reuse the Item item variable that you get in this line:
Item item = repository.GetSingle(transactionItemDto.Id); //find entity first
Note, you'll need to get the entity with tracking on however, vs how your repository GetSingle does it with AsNoTracking. If you use that item (which now has the original/current database values) and map your transactionItemDto properties onto it like this:
var entityToUpdate = mapper.Map<ItemDto, Item>(transactionItemDto);
Then, when you call your repository.Update method passing it entityToUpdate, I believe you'll see the correct before/after values.
.
.
.
.
Old (wrong) answer I originally posted:
In your ApplicationContext code you have the following loop
foreach (var prop in change.OriginalValues.Properties)
I believe that is what is causing your original value/current values to be the same because you are looping over the original values properties. Try changing that loop to:
foreach (var prop in change.Properties)
Then, try reading the values off each property via the prop variable like so:
var currentValue = prop.CurrentValue;
var originalValue = prop.OriginalValue;
EDIT: Ah - I see now that in your code you are trying to read the original value from the change.OriginalValues collection, so I don't think this is going to help.
there are two approaches for this:
var entry = _dbContext.Attach(entity);
var updated = entry.CurrentValues.Clone();
entry.Reload();
entry.CurrentValues.SetValues(updated);
entry.State = EntityState.Modified;
db.SaveChanges();
Or Simply do:
var persondb = db.Persons.Find(person.id);
db.Entry(persondb).CurrentValues.SetValues(person);
db.SaveChanges();
I am using EF5 and I have some entities that I wrote myself and also wrote a function that adds all the mappings to the modelbuilder configurations.
Running a simple test query, I can query items from a table successfully but when I try to add a new item and save, I get an exception that the primary key of my entity is null, even though I gave it a value.
It's quite possible that I messed up the mappings, but I don't know why it would work for a query and not a save.
public class User : IMappedEntity
{
[Key]
[Column("USER_ID")]
public int UserID { get; set; }
[Column("FIRST_NAME")]
public String FirstName { get; set; }
[Column("LAST_NAME")]
public String LastName { get; set; }
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Conventions.Remove<System.Data.Entity.ModelConfiguration.Conventions.PluralizingEntitySetNameConvention>();
modelBuilder.Conventions.Remove<System.Data.Entity.ModelConfiguration.Conventions.PluralizingTableNameConvention>();
var addMethod = (from m in (modelBuilder.Configurations).GetType().GetMethods()
where m.Name == "Add"
&& m.GetParameters().Count() == 1
&& m.GetParameters()[0].ParameterType.Name == typeof(EntityTypeConfiguration<>).Name
select m).First();
if(mappings != null)
{
foreach(var map in mappings)
{
if(map != null && !mappedTypes.Contains(map.GetType()))
{
var thisType = map.GetType();
if (thisType.IsGenericType)
{
thisType = map.GetType().GenericTypeArguments[0];
}
var thisAddMethod = addMethod.MakeGenericMethod(new[] {thisType});
thisAddMethod.Invoke(modelBuilder.Configurations, new[] { map });
mappedTypes.Add(map.GetType());
}
}
}
}
private List<Object> BuildMappings(IEnumerable<Type> types)
{
List<Object> results = new List<Object>();
var pkType = typeof(KeyAttribute);
var dbGenType = typeof(DatabaseGeneratedAttribute);
foreach (Type t in types)
{
String tableName = GetTableName(t);
String schemaName = GetSchema(t);
var mappingType = typeof(EntityTypeConfiguration<>).MakeGenericType(t);
dynamic mapping = Activator.CreateInstance(mappingType);
if (!String.IsNullOrWhiteSpace(schemaName))
mapping.ToTable(tableName, SchemaName.ToUpper());
else
mapping.ToTable(tableName);
var keys = new List<PropertyInfo>();
foreach (PropertyInfo prop in t.GetProperties())
{
String columnName = prop.Name;
if(Attribute.IsDefined(prop, typeof(ColumnAttribute)))
{
columnName = (prop.GetCustomAttribute(typeof(ColumnAttribute)) as ColumnAttribute).Name;
}
if(Attribute.IsDefined(prop, pkType))
keys.Add(prop);
var genFunc = (typeof(Func<,>)).MakeGenericType(t, prop.PropertyType);
var param = Expression.Parameter(t, "t");
var body = Expression.PropertyOrField(param, prop.Name);
dynamic lambda = Expression.Lambda(genFunc, body, new ParameterExpression[] { param });
//if (prop.PropertyType == typeof(Guid) || prop.PropertyType == typeof(Nullable<Guid>))
//{
// mapping.Property(lambda).HasColumnType("Guid");
//}
//else
mapping.Property(lambda).HasColumnName(columnName);
if (Attribute.IsDefined(prop, dbGenType))
mapping.Property(lambda).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Computed);
}
if (keys.Count == 0)
throw new InvalidOperationException("Entity must have a primary key");
dynamic entKey = null;
if(keys.Count == 1)
{
var genFunc = (typeof(Func<,>)).MakeGenericType(t, keys[0].PropertyType);
var param = Expression.Parameter(t, "t");
var body = Expression.PropertyOrField(param, keys[0].Name);
entKey = Expression.Lambda(genFunc, body, new ParameterExpression[] { param });
}
else
{
//if entity uses a compound key, it must have a function named "GetPrimaryKey()" which returns Expression<Func<EntityType,Object>>
//this is because I can't create an expression tree that creates an anonymous type
entKey = t.GetMethod("GetPrimaryKey");
}
mapping.HasKey(entKey);
results.Add(mapping);
}
return results;
}
static void Main(string[] args)
{
using (var ctx = new DQSA.Data.DBContext("DQSATEST"))
{
var xxx = (from u in ctx.Query<DQSA.Data.Entities.User>()
select u).ToList(); //this works, I can see my user
ctx.Set<DQSA.Data.Entities.User>().Add(new DQSA.Data.Entities.User()
{ UserID = 0,
FirstName="Sam",
LastName="Sam"
});
ctx.SaveChanges(); //get an exception here
xxx = (from u in ctx.Query<DQSA.Data.Entities.User>()
select u).ToList();
}
}
It looks like your UserID property is being mapped as an Identity column by convention so the EF query provider thinks it doesn't need to insert that value and the database complains because the field is not nullable.
You can override the convention in your model by using the DatabaseGeneratedAttribute ...
public class User : IMappedEntity
{
[Key]
[Column("USER_ID")]
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public int UserID { get; set; }
...
}
or by removing the convention globally (in your DbContext's OnModelCreating() method) ...
modelBuilder.Conventions.Remove<StoreGeneratedIdentityKeyConvention>();
I think you need to try to seed with one or few records then context.SaveChanges()
by default, entity framework should mark the primary key column of a new table created with Code First as an identity column. Did the database exist prior to your code, or did you use code first to create it?
Can you verify in Management Studio that the column has identity turned on for that field?
In my DbContext, I have an override of the SaveChanges method to generate a Guid for entities to be added to the Database. This works fine except in the case of a shared primary key.
It generates different Id instead of one to be shared by the entities. Is there a way to check the type of Relationship?
public override int SaveChanges()
{
foreach (var entry in ChangeTracker.Entries().Where(e => e.State == EntityState.Added))
{
var t = entry.Entity.GetType();
if (t.GetProperty("Id") == null)
continue;
var info = t.GetProperty("Id").GetCustomAttributes(typeof (DatabaseGeneratedAttribute), true).Cast<DatabaseGeneratedAttribute>();
if (!info.Any() || info.Single().DatabaseGeneratedOption != DatabaseGeneratedOption.Identity)
{
if (t.GetProperty("Id").PropertyType == typeof(Guid) && (Guid)t.GetProperty("Id").GetValue(entry.Entity, null) == Guid.Empty)
t.GetProperty("Id").SetValue(entry.Entity, Guid.NewGuid(), null);
}
}
return base.SaveChanges();
}
I am using nhibernate 3 and fluent nhibernate
I have this enum
[Flags]
public enum Permission
{
View = 1,
Add = 2,
Edit = 4,
Delete = 8,
All = View | Add | Edit | Delete
}
Now say I want to find all users that have "view" or "all".
How could I do this with nhibernate(linq)?
session.Query<TableA>().Where x.Permission == Permission.View); // does not bring back all
session.Query<TableA>().Where x.Permission.HasFlag(Permission.View); // crashes
Is there away to do this with having to go
session.Query<TableA>().Where x.Permission == Permission.View || x.Permission == Permission.All);
Edit
public class TableAMap : ClassMap<TableA>
{
public TableAMap()
{
Id(x => x.Id).GeneratedBy.GuidComb();
Map(x => x.Permission).Not.Nullable().CustomType<PermissionTypes>();
}
}
If you have the following mapping for your TableA type:
public class TableAMap : ClassMap<TableA>
{
public TableAMap()
{
...
Map(x => x.Permission).CustomType(typeof(Permission)).Not.Nullable();
...
}
}
Then you should be able to issue the following query in your repository or wherever you do your data ccess:
public IList<TableA> GetTableByPermission(Permission permission)
{
return Session.Query<TableA>
.Where(x => x.Permission == permission ||
x.Permission == Permission.All)
.ToList();
}
Or you can check whether a single TableA instance has got the required permission:
public bool HasPermission(Guid guid, Permission permission)
{
var tableA = Session.Query<TableA>.SingleOrDefault(x => x.Id == guid);
if (tableA != null)
return (tableA.Permission & permission) == permission;
// Return false or whatever is appropriate.
return false;
}
You cannot use your HasFlag() method in the query, NHibernate does not understand how to use that in the query.