NHibernate Interceptor , auditing and logging to the database - nhibernate

I am using this piece of code as a part of my auditTrail Class but I am facing a stackOverFlow Exception when I try to log the changes to the database.
public void OnPostInsert(NHibernate.Event.PostInsertEvent #event)
{
if (!(#event.Entity is IAuditable))
{
using (ITransaction transaction = #event.Session.BeginTransaction())
{
if (#event.State != null)
{
for (int i = 0; i < #event.State.Length; i++)
{
string propertyName = #event.Persister.PropertyNames[i];
if (#event.State[i] != null)
{
if (#event.State[i].GetType().Namespace.StartsWith("Averma.Fda.Domain"))
{
CompareIfOldStateIsNull(#event.State[i], Convert.ToInt32(#event.Id), #event.Entity.GetType().ToString(), #event.Session, false);
}
else
{
string auditEntry = "New value for " + SplitPropertyName(propertyName) + " has been added, the new value is " + #event.State[i];
#event.Session.Save(new AuditTrail() { AuditEntry = auditEntry, RelatedEntityId = Convert.ToInt32(#event.Id), RelatedEntityType = #event.Entity.GetType().ToString(), OperationType = Shared.Enums.OperationType.Insert });
}
}
}
}
transaction.Commit();//the error occurs here
}
}
}
Could anyone guide me about how to resolve this issue and how can I log the changes to the database .

Do not begin a new transaction and commit it inside the NHibernate interceptor because a transaction is already open and will be committed after the interceptor finishes its work, all what you want to do is to remove using (ITransaction transaction = #event.Session.BeginTransaction()) and to remove transaction.Commit(); and things will be ok.

The problem is when you save your audit entry, it tries to create another audit log entry for the first audit entry and it repeats. The fix would be to check the target table is audit log table and not to create entry if it's the case.

Related

ASP.NET Core + Entity Framework changed data

I'm trying to save only changed entities.
If I remove this if:
if (!period.IsSame(_context.Periods.First(p => p.ID == period.ID)))
everything is fine.
But if I keep it, on the statement _context.Attach(period); or same if I use Update, I get an error:
InvalidOperationException: The instance of entity type 'Period' cannot be tracked because another instance with the same key value for {'ID'} is already being tracked.
I don't know how test that it's really modified.
public async Task<IActionResult> OnPostAsync(List<Period> periods)
{
if (!ModelState.IsValid)
{
return Page();
}
// _context.Periods.Add(Period);
int i = 0;
foreach (var period in periods)
{
TimeOnly startTime = TimeOnly.Parse(Request.Form["StartTime" + i].ToString());
TimeOnly endTime = TimeOnly.Parse(Request.Form["EndTime" + i].ToString());
period.StartHour = startTime.Hour;
period.StartMinute = startTime.Minute;
period.EndHour = endTime.Hour;
period.EndMinute = endTime.Minute;
period.StatusDate = DateTime.Now;
// if it already exists
if (period.ID > 0)
{
// if modified
if (!period.IsSame(_context.Periods.First(p => p.ID == period.ID)))
{
_context.Attach(period);
if (period.Delete)
{
period.Status = (int)Status.deleted;
}
else
{
period.Status = (int)Status.modified;
}
}
}
// if new
else
{
period.Status = (int)Status.created;
_context.Attach(period);
}
i++;
}
await _context.SaveChangesAsync();
return RedirectToPage("./Periods");
}
I have tried both update and attach. I have search for entity tracking but it seems to be detached as soon as it's on a webpage
EF uses concept of change tracking to determine what should be done with entities. By default querying data will lead to context starting to track it hence the exception. You can mitigate it by disabling tracking by default, for example using .AsNoTracking():
if (!period.IsSame(_context.Periods.AsNoTracking().First(p => p.ID == period.ID)))
{
// ...
}
But this is not very advisable approach, due to multiple reasons - possibly change tracker will not detect any changes and you will need to handle that manually, and bigger reason - you will be querying database in a loop which is bad for application performance. Just fetch everything from the database and update it accordingly:
var existingPeriods = await _context.Periods
.Where(p => periods.Select(p => p.ID).Contains(p.ID))
.ToListAsync(); // or ToDictionaryAsync if there a lot of the periods
foreach (var period in periods)
{
var existing = existingPeriods.FirstOrDefault(p => p.ID == period.ID);
if (existing != null)
{
// maybe throw if period.ID != 0
// update data in existing
existing. ... = ...;
}
else
{
// if new ...
_context.Periods.Add(period);
}
await _context.SaveChangesAsync();
}

entity framework 5 change log how to implement?

I am creating an application with MVC4 and entity framework 5. How do can I implement this?
I have looked around and found that I need to override SaveChanges .
Does anyone have any sample code on this? I am using code first approach.
As an example, the way I am saving data is as follows,
public class AuditZoneRepository : IAuditZoneRepository
{
private AISDbContext context = new AISDbContext();
public int Save(AuditZone model, ModelStateDictionary modelState)
{
if (model.Id == 0)
{
context.AuditZones.Add(model);
}
else
{
var recordToUpdate = context.AuditZones.FirstOrDefault(x => x.Id == model.Id);
if (recordToUpdate != null)
{
recordToUpdate.Description = model.Description;
recordToUpdate.Valid = model.Valid;
recordToUpdate.ModifiedDate = DateTime.Now;
}
}
try
{
context.SaveChanges();
return 1;
}
catch (Exception ex)
{
modelState.AddModelError("", "Database error has occured. Please try again later");
return -1;
}
}
}
There is no need to override SaveChanges.
You can
Trigger Context.ChangeTracker.DetectChanges(); // may be necessary depending on your Proxy approach
Then analyze the context BEFORE save.
you can then... add the Change Log to the CURRENT Unit of work.
So the log gets saved in one COMMIT transaction.
Or process it as you see fit.
But saving your change log at same time. makes sure it is ONE Transaction.
Analyzing the context sample:
I have a simple tool, to Dump context content to debug output so when in debugger I can use immediate window to check content. eg
You can use this as a starter to prepare your CHANGE Log.
Try it in debugger immediate window. I have FULL dump on my Context class.
Sample Immediate window call. UoW.Context.FullDump();
public void FullDump()
{
Debug.WriteLine("=====Begin of Context Dump=======");
var dbsetList = this.ChangeTracker.Entries();
foreach (var dbEntityEntry in dbsetList)
{
Debug.WriteLine(dbEntityEntry.Entity.GetType().Name + " => " + dbEntityEntry.State);
switch (dbEntityEntry.State)
{
case EntityState.Detached:
case EntityState.Unchanged:
case EntityState.Added:
case EntityState.Modified:
WriteCurrentValues(dbEntityEntry);
break;
case EntityState.Deleted:
WriteOriginalValues(dbEntityEntry);
break;
default:
throw new ArgumentOutOfRangeException();
}
Debug.WriteLine("==========End of Entity======");
}
Debug.WriteLine("==========End of Context======");
}
private static void WriteCurrentValues(DbEntityEntry dbEntityEntry)
{
foreach (var cv in dbEntityEntry.CurrentValues.PropertyNames)
{
Debug.WriteLine(cv + "=" + dbEntityEntry.CurrentValues[cv]);
}
}
private static void WriteOriginalValues(DbEntityEntry dbEntityEntry)
{
foreach (var cv in dbEntityEntry.OriginalValues.PropertyNames)
{
Debug.WriteLine(cv + "=" + dbEntityEntry.OriginalValues[cv]);
}
}
}
EDIT: Get the changes
I use this routine to get chnages...
public class ObjectPair {
public string Key { get; set; }
public object Original { get; set; }
public object Current { get; set; }
}
public virtual IList<ObjectPair> GetChanges(object poco) {
var changes = new List<ObjectPair>();
var thePoco = (TPoco) poco;
foreach (var propName in Entry(thePoco).CurrentValues.PropertyNames) {
var curr = Entry(thePoco).CurrentValues[propName];
var orig = Entry(thePoco).OriginalValues[propName];
if (curr != null && orig != null) {
if (curr.Equals(orig)) {
continue;
}
}
if (curr == null && orig == null) {
continue;
}
var aChangePair = new ObjectPair {Key = propName, Current = curr, Original = orig};
changes.Add(aChangePair);
}
return changes;
}
edit 2 If you must use the Internal Object tracking.
var context = ???// YOUR DBCONTEXT class
// get objectcontext from dbcontext...
var objectContext = ((IObjectContextAdapter) context).ObjectContext;
// for each tracked entry
foreach (var dbEntityEntry in context.ChangeTracker.Entries()) {
//get the state entry from the statemanager per changed object
var stateEntry = objectContext.ObjectStateManager.GetObjectStateEntry(dbEntityEntry.Entity);
var modProps = stateEntry.GetModifiedProperties();
Debug.WriteLine(modProps.ToString());
}
I decompiled EF6 . Get modified is indeed using private bit array to track fields that have
been changed.
// EF decompiled source..... _modifiedFields is a bitarray
public override IEnumerable<string> GetModifiedProperties()
{
this.ValidateState();
if (EntityState.Modified == this.State && this._modifiedFields != null)
{
for (int i = 0; i < this._modifiedFields.Length; ++i)
{
if (this._modifiedFields[i])
yield return this.GetCLayerName(i, this._cacheTypeMetadata);
}
}
}

NHibernate: Evicting but still having non unique object exception

This method is not my best, but had a circular reference issue going on so slapped it together last minute. For some reason, even though I'm evicting the original referenced order on the detail object, I've still got another association with the session. Should I use a get instead? Or even better is there a way to say evict ALL orders with ID = x ?
public DetailDTO SaveNewDetailToOrder(DetailDTO detailDTO)
{
var detailReturn = new DetailDTO();
try
{
var order = LoadOrderById(detailDTO.OrderId);
var previousStatus = issue.CurrentDetailStatus;
if (previousStatus != null && detailDTO.Status.Id != previousStatus.Id)
{
var detail = Mapper.Map<DetailDTO, Detail>(detailDTO);
_orderRepository.EvictOrder(detail.DetailOrder);
order.Details.Add(detailDTO);
order.IsEscalated = false;
order.DormantDate = detailDTO.CreatedTime;
var orderReturn = SaveOrder(order); ///Error Here
if (orderReturn.IsActionSuccessful)
{
detailReturn =
orderReturn.Details.DTOObjects.OrderByDescending(x => x.CreatedTime).First();
SendStatusChangeEmail(orderReturn);
}
}
else
{
detailReturn = _detailService.SaveDetail(detailDTO);
}
}
catch (Exception ex)
{
throw ServiceErrorMessage(ex, detailReturn);
}
return detailReturn ;
}
You can access session objects and use then whatever you like
session.GetSessionImplementation().PersistenceContext.EntityEntries
but if I were you i would make sure that i'm evicting the right object and spend some time on debuging. Knowing what is going on is better than searching for workarounds
foreach (var e in session.GetSessionImplementation().PersistenceContext.EntityEntries.Values.OfType<EntityType>().Where(<condition>))
{
session.Evict(e);
}

When to call NHibernate Rollback?

I'm using NHibernate to insert some data into Table A. I want to update the status in Table B if the Table A transaction fails. How do I check if it has failed?
Below is my current code:
// Add userId to Receiver
Receiver receiver = new Receiver();
receiver.User = User.GetById(Convert.ToInt32(listItem.Value));
receiver.Notification = Notification.GetById(notification.NotificationId);
receiver.Save();
Where do I call the NHibernate Transaction? If it fails where do I call NHibernate Rollback and update the Table B status?
Take a look at the Official NHibernate documentation on Exception handling: http://nhibernate.info/doc/nh/en/index.html#manipulatingdata-exceptions
using ( ISession session = sessionFactory.OpenSession() )
{
using ( ITransaction transaction = session.BeginTransaction() )
{
try
{
// Do your save/update here for Table A
transaction.Commit();
}
catch( Exception e )
{
// Your save or update failed so this is where you
// could capture that info and update your Table B
transaction.Rollback();
}
}
}
From what I remember, you don't actually have to call tx.Rollback() because when your code leaves the using blocks, it will do that automatically but again, I can't remember exactly. Give it a try and if it doesn't behave as I just described, you can manually rollback in the catch.
using (ISession session = factory.OpenSession())
using (ITransaction tx = session.BeginTransaction())
{
// do some work
tx.Commit();
}
or manually
ISession session = factory.openSession();
try
{
// do some work
session.Flush();
currentTransaction.Commit();
}
catch (Exception e)
{
currentTransaction.Rollback();
throw;
}
finally
{
session.Close();
}
Take a look NHibernate transactions for more details

Rollback transaction in Repository from another class

well my problem is:
I have a method like:
class Manager
{
void method1()
{
// save object in database to get ID
int newId = this.Repository.Save(obj);
try {
// call remote webservice to save another object with same ID as in local DB
webservice.Save(remoteObj, id);
}
catch(Exception e)
{
// do Rollback in Repository here
}
}
}
Bassically this is the code. Repository use NHibernate to save to DB. I need to save in DB to know the new ID and then send this ID to webservice. If something fail calling webservice I want to rollback and discard saved object.... and here is my problem. I can't open and control a transaction in Repository from my class Manager.
I already try with this also:
class Manager
{
void method1()
{
using (TransactionScope scope = new TransactionScope())
{
// save object in database to get ID
int newId = this.Repository.Save(obj);
// call remote webservice to save another object with same ID
// as in local DB
webservice.Save(remoteObj, id);
scope.Complete();
}
}
}
Here the problem is that the rollback is OK but not the Save(Create in NHibernate). I get error about that object "Transaction" is not found or the transaction is already closed just after the line : "scope.Complete();".
I think that something is wrong trying to control NHibernate transaction with TransactionScope .
I dont know if is a problem about approach, maybe another way should be used to handle this situation... ??
any help or idea where to find ??
Thanks a lot !!
Assuming you already have an opened session in a CurrentSession property/variable and that you could pass that working session to your repository, I would do the following:
using(var trx = CurrentSession.BeginTransaction())
{
try
{
int newId = this.Repository.Save(obj, CurrentSession);
webservice.Save(remoteObj, id);
trx.Commit();
}
catch
{
trx.Rollback();
}
}