I'm having trouble getting the Id of an object passed by parameter.
The code:
private void SetEntityState(TEntity entity, EntityState entityStated)
{
var entry = this.Context.Entry(entity);
if (entry.State == EntityState.Detached)
{
// The problem is here.
TEntity attachedEntity = this.DbSet.Where(
x => x.Id.Equals(entity.Id)
).FirstOrDefault();
if (attachedEntity != null)
{
var attachedEntry = this.Context.Entry(attachedEntity);
attachedEntry.CurrentValues.SetValues(entity);
attachedEntry.State = entityStated;
}
else
{
entry.State = entityStated;
}
}
}
The problem is here:
TEntity attachedEntity = this.DbSet.Where(
x => x.Id.Equals(entity.Id)
).FirstOrDefault();
The property 'Id' of the 'entity' is always returning 0 but in debbug, I can see that the value is 3.
Why is this happening?
Maybe you can try (for testing):
var entry = Context.Find(entity);
instead of
var entry = this.Context.Entry(entity);
Try this
TEntity attachedEntity = this.DbSet.Where(x => x.Id == entity.Id).FirstOrDefault();
OR
Int32 entityId = entity.Id;
TEntity attachedEntity = this.DbSet.Where(x => x.Id == entityId).FirstOrDefault();
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!
So, we created this project: https://github.com/efonsecab/BlazorRestaurant
The EF Core logic for SaveChanges is extended to automatically fill the data for the auditing columns.
Logic is in the BlazorRestaurantDbContext.partial.cs file.
public partial class BlazorRestaurantDbContext
{
public override int SaveChanges()
{
ValidateAndSetDefaults();
return base.SaveChanges();
}
public override int SaveChanges(bool acceptAllChangesOnSuccess)
{
ValidateAndSetDefaults();
return base.SaveChanges(acceptAllChangesOnSuccess);
}
public override Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default)
{
ValidateAndSetDefaults();
return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
}
public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
ValidateAndSetDefaults();
return base.SaveChangesAsync(cancellationToken);
}
private void ValidateAndSetDefaults()
{
//Check https://www.bricelam.net/2016/12/13/validation-in-efcore.html
var entities = from e in ChangeTracker.Entries()
where e.State == EntityState.Added
|| e.State == EntityState.Modified
select e.Entity;
string ipAddresses = String.Empty;
string assemblyFullName = String.Empty;
string rowCretionUser = String.Empty;
if (entities.Any(p => p is IOriginatorInfo))
{
ipAddresses = String.Join(",", GetCurrentHostIPv4Addresses());
assemblyFullName = System.Reflection.Assembly.GetEntryAssembly().FullName;
if (Thread.CurrentPrincipal != null && Thread.CurrentPrincipal.Identity != null)
rowCretionUser = Thread.CurrentPrincipal.Identity.Name;
else
rowCretionUser = "Unknown";
}
foreach (var entity in entities)
{
if (entity is IOriginatorInfo)
{
IOriginatorInfo entityWithOriginator = entity as IOriginatorInfo;
if (String.IsNullOrWhiteSpace(entityWithOriginator.SourceApplication))
{
entityWithOriginator.SourceApplication = assemblyFullName;
}
if (String.IsNullOrWhiteSpace(entityWithOriginator.OriginatorIpaddress))
{
entityWithOriginator.OriginatorIpaddress = ipAddresses;
}
if (entityWithOriginator.RowCreationDateTime == DateTimeOffset.MinValue)
{
entityWithOriginator.RowCreationDateTime = DateTimeOffset.UtcNow;
}
if (String.IsNullOrWhiteSpace(entityWithOriginator.RowCreationUser))
{
try
{
entityWithOriginator.RowCreationUser = rowCretionUser;
}
catch (Exception)
{
entityWithOriginator.RowCreationUser = "Unknown";
}
}
}
var validationContext = new ValidationContext(entity);
Validator.ValidateObject(
entity,
validationContext,
validateAllProperties: true);
}
}
public static List<string> GetCurrentHostIPv4Addresses()
{
//Check https://stackoverflow.com/questions/50386546/net-core-2-x-how-to-get-the-current-active-local-network-ipv4-address
// order interfaces by speed and filter out down and loopback
// take first of the remaining
var allUpInterfaces = NetworkInterface.GetAllNetworkInterfaces()
.OrderByDescending(c => c.Speed)
.Where(c => c.NetworkInterfaceType != NetworkInterfaceType.Loopback &&
c.OperationalStatus == OperationalStatus.Up).ToList();
List<string> lstIps = new();
if (allUpInterfaces != null && allUpInterfaces.Count > 0)
{
foreach (var singleUpInterface in allUpInterfaces)
{
var props = singleUpInterface.GetIPProperties();
// get first IPV4 address assigned to this interface
var allIpV4Address = props.UnicastAddresses
.Where(c => c.Address.AddressFamily == AddressFamily.InterNetwork)
.Select(c => c.Address)
.ToList();
allIpV4Address.ForEach((IpV4Address) =>
{
lstIps.Add(IpV4Address.ToString());
});
}
}
return lstIps;
}
}
We have used this same code in other apps and works, great.
In this specific app, however, the System.Threading.Thread.CurrentPrincipal and ClaimsPrincipal.Current, are null, when the ValidateAndSetDefaults method is executed.
Even tried the following
services.Configure<JwtBearerOptions>(
JwtBearerDefaults.AuthenticationScheme, options =>
{
options.TokenValidationParameters.NameClaimType = "name";
options.Events.OnTokenValidated = (context) =>
{
System.Threading.Thread.CurrentPrincipal = context.Principal;
return Task.CompletedTask;
};
options.SaveToken = true;
});
The context.Principal does have all the data we need at this point, but we still get null reading the Thread's CurrenPrincipal object.
Any ideas how to fix it, so we can properly get the Authenticated User information in the code above, it exists in context.Principal, it is just not accesible later in the SaveChanges for EF Core.
Fixed by using Dependency Injection along with HttpContextAccesor
https://github.com/efonsecab/BlazorRestaurant/blob/main/src/BlazorRestaurantSln/BlazorRestaurant.DataAccess/Data/BlazorRestaurantDbContext.partial.cs
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'm trying to implement a generic many-to-many Update method using EF6.
Every time I try to update the navigation property, I get an exception:
"Violation of PRIMARY KEY constraint 'PK_dbo.Classes'. Cannot insert
duplicate key in object 'dbo.Classes'. The duplicate key value is
(698d5483-eb48-4d7e-84e7-a9f95a243d3d).\r\nThe statement has been
terminated."
How can I fix it?
public void Update(T entity, params Expression<Func<T, IEnumerable>>[] navProps)
{
using (var context = new Context())
{
T foundEntity = context.Set<T>().Find(entity.Id);
var entry = context.Entry(foundEntity);
entry.CurrentValues.SetValues(entity);
foreach (var prop in navProps)
{
string memberName;
var member = prop.Body as MemberExpression;
if (member != null)
{
memberName = member.Member.Name;
}
else
{
throw new Exception();
}
var collection = entry.Collection(memberName);
collection.Load();
collection.CurrentValue = typeof(T).GetProperty(memberName).GetValue(entity);
}
context.SaveChanges();
}
}
Finally I have found a solution for generic many-to-many update
public void Update(Guid id, T record)
{
using (var context = new FortisDataStoreContext(_client, _user))
{
var set = context.Set<T>().AsQueryable();
foreach (var x in GetNavigationProperties(context, record))
{
foreach (var xx in x.PropertyType.GetGenericArguments())
{
set = set.Include(x.Name);
}
}
var entry = set.FirstOrDefault(x => x.Id == id && x.IsClosed == false);
context.Entry(entry).CurrentValues.SetValues(record);
foreach (var x in GetNavigationProperties(context, record))
{
if(x.PropertyType.GetGenericArguments().Count() > 0)
{
var genericType = x.PropertyType.GenericTypeArguments[0];
var newValues = (IList)x.GetValue(record, null) ?? new object[0];
var genericList = CreateList(genericType);
foreach (var item in newValues)
{
var ident = item.GetType().GetProperty("Id").GetValue(item, null);
var obj = context.Set(genericType).Find(ident);
genericList.Add(obj);
}
context.Entry(entry).Collection(x.Name).CurrentValue = genericList;
}
}
context.SaveChanges();
}
}
I'm using MVC C# and entity frame work.
So in my partial view I want to change data.
This is my controller
public ActionResult BusinessDetails(){
int bsId = bskey ?? default(int);
var BusinessQuery = GetBusinessById(bsId);
var PeopleQuery = BusinessQuery.t_PEOPLE
.Where(p => p.PP_IS_CONTACT == true).AsEnumerable();
var peopleCollection = new EntityCollection<t_PEOPLE>();
foreach (var m in PeopleQuery)
{
//This is problem.-----------
peopleCollection.Add(m);
} //--------------
BusinessQuery.t_PEOPLE = peopleCollection;
return PartialView("_EditBusiness", BusinessQuery);
}
return RedirectToAction("Index");
}
public t_BUSINESS GetBusinessById(int id){
if (id == 0)
return null;
var query = from b in db.t_BUSINESS
where b.BS_KEY == id
select b;
var Business = query.FirstOrDefault();
return Business;
}
So how can I assign the PeopleQuery to my BusinessQuery.t_PEOPLE.
Ther is error and says
The object could not be added to the EntityCollection or EntityReference. An object that is attached to an ObjectContext cannot be added to an EntityCollection or EntityReference that is not associated with a source object
You could directly query the records you are looking for:
public ActionResult BusinessDetails()
{
int bsId = bskey ?? default(int);
var business = GetContactBusinessById(bsId);
return PartialView("_EditBusiness", business);
}
public t_BUSINESS GetContactBusinessById(int id)
{
if (id == 0)
{
return null;
}
var query =
from b in db.t_BUSINESS
from p in b.t_PEOPLE
where b.BS_KEY == id && p.PP_IS_CONTACT
select b;
return query.FirstOrDefault();
}
try this
var PeopleQuery = BusinessQuery.t_PEOPLE.Where(p => p.PP_IS_CONTACT == true).ToList();