thepirat000 / Audit.NET

An extensible framework to audit executing operations in .NET and .NET Core.
MIT License
2.27k stars 323 forks source link

Related object causes multiple audit entries #619

Closed kensleebos closed 1 year ago

kensleebos commented 1 year ago

Describe the bug I have a object with child objects, the parent is being updated. When this occurs there are multiple audit entries created with Updates and Deletes. No child is actualy being deleted.

So when a child is updated the following audits are being inserted. In this example no child delete action is invoked. Initial:

Update request:

New Audit Record: Update Parent Update Child 1 Update Child 2 Delete Child 1 Delete Child 2

This doesnt make sense.

My Classes:

public class RandomDataList
  {
    public int Id { get; set; }
    public string Name { get; set; }
    public virtual ICollection<RandomDataListItem> Items { get; set; }
  }

public class RandomDataListItem
  {
    public int Id { get; set; }
    public string Name { get; set; }
    public int RandomDataListId { get; set; }
    public virtual RandomDataList RandomDataList { get; set; }
  } 

My Config:

Audit.Core.Configuration.Setup()
        .UseEntityFramework(x => x
          .AuditTypeMapper(t => typeof(AuditLog))
          .AuditEntityAction<AuditLog>((ev, entry, entity) =>
          {
            entity.EntityType = entry.EntityType.Name;
            entity.AuditDate = DateTime.Now;
            entity.AuditUser = Environment.UserName;
            entity.TablePk = entry.PrimaryKey.First().Value.ToString();

            // remove all the changes where the old and new values are the same
            if (entry.Changes != null)
            {
              entry.Changes.RemoveAll(change => Equals(change.OriginalValue, change.NewValue));
            }

            entity.AuditData = entry.ToJson();
          })
          .IgnoreMatchedProperties(true));

      Audit.EntityFramework.Configuration.Setup()
      .ForAnyContext(_ => _
        .IncludeEntityObjects()
        .ReloadDatabaseValues()
        .AuditEventType("{context}:{database}"))
      .UseOptOut();

To Reproduce Update/Insert/Delete any item in the list, but update the parent object with the list, aka:

var randomDataList = _repository.ById(request.Entity.Id);
      if (randomDataList == null)
      {
        throw new InvalidDataException("Entity does not exist.");
      }

      _mapper.Map(request.Entity, randomDataList);

      _repository.Update(randomDataList);
      _unitOfWork.Save();

Expected behavior I dont want so many audit entries, its not logical, i only want the main object update audit or the specific child object audit. Also it doesnt make sense that it creates Delete Audits

Libraries :

Target .NET framework:

thepirat000 commented 1 year ago

Can you create and share a minimal-reproducible-example ?

Since I don't know what the _repository.ById, _mapper, and _reposutory.Update do, it's impossible to say what could be wrong.

Can you try enabling logging for SQL statements in your DbContext so that you can check if any delete is happening?

kensleebos commented 1 year ago

I use a generic repository that does the following:

public T? ById(object id, Func<IQueryable<T?>, IIncludableQueryable<T, object>>? includes = null)
    {
      var type = Context.Model.FindEntityType(typeof(T));
      var primaryKey = type?.FindPrimaryKey();

      if (primaryKey == null)
      {
        throw new ArgumentException("Entity does not have any primary key defined.");
      }

      var properties = primaryKey.Properties;
      var primaryKeyName = properties.Select(p => p.Name).FirstOrDefault();
      var primaryKeyType = properties.Select(p => p.ClrType).FirstOrDefault();

      object? primaryKeyValue = null;

      try
      {
        if (primaryKeyType != null)
        {
          primaryKeyValue = Convert.ChangeType(id, primaryKeyType, CultureInfo.InvariantCulture);
        }
      }
      catch (Exception)
      {
        throw new ArgumentException(
          $"You can not assign a value of type {id.GetType()} to a property of type {primaryKeyType}");
      }

      var pe = Expression.Parameter(typeof(T), "entity");
      if (primaryKeyName == null)
      {
        return null;
      }

      var me = Expression.Property(pe, primaryKeyName);
      if (primaryKeyType == null)
      {
        return null;
      }

      var constant = Expression.Constant(primaryKeyValue, primaryKeyType);
      var body = Expression.Equal(me, constant);
      var expressionTree = Expression.Lambda<Func<T, bool>>(body, pe);

      IQueryable<T?> query = Context.Set<T>();

      includes?.Invoke(query).Load();

      return query.FirstOrDefault(expressionTree!);
    }

     public void Update(T entityToUpdate)
    {
      DbSet.Update(entityToUpdate);
      Context.Entry(entityToUpdate).State = EntityState.Modified;
    }    

The mapper is just automapper : https://automapper.org/

      CreateMap<RandomDataList, RandomDataListDto>().ReverseMap();
      CreateMap<RandomDataListItem, RandomDataListItemDto>().ReverseMap();

The DTO models are exactly the same as the entity models

My database config:

services.AddDbContext<InstanceCreatorContext>(
        o =>
        {
          o.UseSqlServer(configuration.GetConnectionString("DefaultConnection"));
          o.EnableDetailedErrors();
          o.AddInterceptors(new AuditSaveChangesInterceptor());
          o.EnableSensitiveDataLogging();
          o.UseLazyLoadingProxies(false);
        });

I checked the queries output and there is no delete statement

kensleebos commented 1 year ago

i found the problem, its entity framework that doesn't like it when you update the children trough the parent.

https://github.com/dotnet/efcore/issues/23158