4

I'm using Audit.NET with the EntityFramework extension, and everything was running fine when I was tracking just 1 entity.

Now I'm tracking another entity as well that is connected to that first entity, and when I try to save it, the audit save function throws a Reflection error

System.Reflection.TargetException: 'Object does not match target type.'

The structure of my classes is like so:

public class FirstClass{
  public int ID{get;set;}
  //Some props here
  public SecondClass SecondClass{get;set}
}

public class SecondClass{
  public int ID{get;set;}
  public int FirstClassId{get;set;}
  public MySpecialClass JustForData{get;set;}
  //Some other props here
}

Then I just modeled my Audit classes to be the exact same but with added audit fields

public class AuditClass{
  public Guid AuditId{get;set;}
  public string AuditMessage{get;set;}
}

public class FirstClassAudit : AuditClass{
  public int ID{get;set;}
  //Some props here
  //No SecondClass prop here
}

public class SecondClassAudit: AuditClass{
  public int ID{get;set;}
  public int FirstClassId{get;set;}
  public MySpecialClass JustForData{get;set;}
  //Some other props here
}

And then left out the reference to SecondClass in FirstClassAudit

Both of my classes are in the DbContext, the audit classes are each mapped to a separate table. I added the mappings for both classes under an AuditTypeExplicitMapper, which I debugged through without issue. And yet I still get an error on the SaveChanges function

This does not seem to happen when I leave the SecondClass reference as null when I save

EDIT: Some more information

Audit.NET config:

Audit.Core.Configuration.Setup()
                .UseEntityFramework(
                ef => ef
                    .AuditTypeExplicitMapper(m => m
                    .Map<FirstClass, FirstClassAudit>((frst, auditFrst) =>
                    {
                        //Map the tag fields in here
                        auditFrst.Tag = frst.Installation.Tag;
                        //Some more props here
                    })
                    .Map<SecondClass, SecondClassAudit>()
                    .AuditEntityAction((ev, ent, auditEntity) =>
                    {
                        ((AuditClass)auditEntity).AuditMessage = ent.Action;
                    }))
                );

Save function in DbContext:

public override int SaveChanges()
        {
            return Helper.SaveChanges(auditContext, () => base.SaveChanges());
        }

EDIT 2: Stack trace

at System.Reflection.RuntimeMethodInfo.CheckConsistency(Object target) at System.Reflection.RuntimeMethodInfo.InvokeArgumentsCheck(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture) at System.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture) at System.Reflection.RuntimePropertyInfo.GetValue(Object obj, Object[] index) at Audit.EntityFramework.Providers.EntityFrameworkDataProvider.CreateAuditEntity(Type definingType, Type auditType, EventEntry entry) at Audit.EntityFramework.Providers.EntityFrameworkDataProvider.InsertEvent(AuditEvent auditEvent) at Audit.Core.AuditScope.SaveEvent(Boolean forceInsert) at Audit.Core.AuditScope.Save() at Audit.EntityFramework.DbContextHelper.SaveScope(IAuditDbContext context, AuditScope scope, EntityFrameworkEvent event) at Audit.EntityFramework.DbContextHelper.SaveChanges(IAuditDbContext context, Func`1 baseSaveChanges) at MyDbContext.SaveChanges() in [MyLocalPath]\MyDbContext.cs:line 132 at FirstClassRepository.UpdateFirstClass(Int32 id, FirstClassDto first) in [MyLocalPath]\FirstClassRepository.cs:line 209 at FirstClassManager.UpdateFirstClass(Int32 id, FirstClassDto dto) in [MyLocalPath]\FirstClassManager.cs:line 244 at FirstClassController.<>c__DisplayClass20_0.b__0() in [MyLocalPath]\FirstClassController.cs:line 249

EDIT: After fiddling around a bit more I got the error to say what type it was by adding 'MySpecialClass' to the mappings

System.ArgumentException: 'Object of type 'MySpecialClass' cannot be converted to type 'AuditMySpecialClass'.'

This class is an Owned Type within my datacontext, this may have something to do with it, maybe not.

Right now the error seems to get thrown before it gets to the user defined action that you can add in the mapping, possibly Audit.NET is trying to map these things before the user defined action?

Firenter
  • 301
  • 3
  • 14
  • 1
    I think we need a more complete sample of your code, ideally a [mre]. I don't think we have enough right now. –  Jan 15 '20 at 14:29
  • Include, at least, your configuration calls (`Audit.Core.Configuration.Setup()` / `Audit.EntityFramework.Configuration.Setup()`), your save function and the complete stacktrace. – thepirat000 Jan 15 '20 at 16:28
  • include Helper.SaveChanges( – Seabizkit Jan 16 '20 at 08:31
  • @Seabizkit Helper.SaveChanges is part of Audit.NET, I have no control over it – Firenter Jan 16 '20 at 08:49
  • Include the code for `FirstClassAudit` and `SecondClassAudit` I guess there is a property with the same name but different type between your Class and your Audit Class. – thepirat000 Jan 16 '20 at 18:55
  • @thepirat000 This does not seem likely to me as I FirstClassAudit on its own works perfectly fine, it doesn't start going wrong until after I add SecondClassAudit – Firenter Jan 17 '20 at 11:08
  • [This](https://github.com/thepirat000/Audit.NET/blob/437bebca365e86cdece46833a17c572b61cd6d7a/src/Audit.EntityFramework/Providers/EntityFrameworkDataProvider.cs#L205) is the line that is throwing the exception. The only case I could see is that both classes has a property with the same name and different type. – thepirat000 Jan 17 '20 at 15:40
  • By default, the EntityFramework Data Provider on `Audit.EntityFramework` library will try to match properties between your classes and your audit classes by property name. You can disable this behavior with [`IgnoreMatchedProperties` setting](https://github.com/thepirat000/Audit.NET/blob/master/src/Audit.EntityFramework/README.md#ef-provider-options) but you will need to manually specify the mapping for each property/class in your context. – thepirat000 Jan 17 '20 at 19:51
  • @thepirat000 Annoying that you have to do it context-wide, would have been nice to do it on a object level, but this is probably going to be the way to go I think – Firenter Jan 17 '20 at 22:58
  • Maybe I can change the setting to be a Func of the audited entity type, so you can choose to ignore or not by audited entity type, I'll evaluate the impact – thepirat000 Jan 18 '20 at 22:39

2 Answers2

1

With the latest version of Audit.EntityFramework (15.0.2), you can now ignore property matching only for certain audit types, as follows:

Audit.Core.Configuration.Setup()
    .UseEntityFramework(ef => ef
        .AuditTypeExplicitMapper(m => m
          .Map<FirstClass, FirstClassAudit>((frst, auditFrst) =>
          {
            auditFrst.Tag = frst.Installation.Tag;
          })
          .Map<SecondClass, SecondClassAudit>()
          .AuditEntityAction((ev, ent, auditEntity) =>
          {
            ((AuditClass)auditEntity).AuditMessage = ent.Action;
          }))
        .IgnoreMatchedProperties(t => t == typeof(FirstClassAudit)) // <-- Ignore prop. matching for FirstClassAudit
    );
thepirat000
  • 12,362
  • 4
  • 46
  • 72
0

So, I found a solution. It's not 100% how I would like, but it works.

The problem was with my "MySpecialClass" objects as they were owned types withing EFCore, they generated their own separate events, which confused Audit.NET

So I added [AuditIgnore] above the "MySpecialClass" declaration and added IgnoreMatchedProperties to the configuration

[AuditIgnore]
public class MySpecialClass
{
  public Unit? UnitOfMeasure { get; set; }

  public float? Value { get; set; }
}
Audit.Core.Configuration.Setup()
                .UseEntityFramework(
                ef => ef
                    .AuditTypeExplicitMapper(m => m
                    .Map<FirstClass, FirstClassAudit>((frst, auditFrst) =>
                    {
                        MapMatchedProperties(frst, auditFrst);
                        //Map the tag fields in here
                        auditFrst.Tag = frst.Installation.Tag;
                        //Some more props here
                    })
                    .Map<SecondClass, SecondClassAudit>((scnd, auditScnd)=>
                    {
                        MapMatchedProperties(scnd, auditScnd);
                    })
                    .AuditEntityAction((ev, ent, auditEntity) =>
                    {
                        ((AuditClass)auditEntity).AuditMessage = ent.Action;
                    }))
                    .IgnoreMatchedProperties()
                );

Also I added my own mapping function "MapMatchedProperties" to correctly map every single field with special exceptions for "MySpecialClass"

private static void MapMatchedProperties(object source, object destination)
        {
            var sourceType = source.GetType();
            var destinationType = destination.GetType();

            var sourceFields = sourceType.GetProperties();
            var destinationFields = destinationType.GetProperties();

            foreach (var field in sourceFields)
            {
                var destinationField = destinationFields.FirstOrDefault(f => f.Name.Equals(field.Name));

                if (destinationField != null && (destinationField.PropertyType == field.PropertyType))
                {
                    //Normal field
                    var sourceValue = field.GetValue(source);

                    destinationField.SetValue(destination, sourceValue);
                } else if(destinationField != null && (destinationField.PropertyType == typeof(AuditMySpecialClass) && field.PropertyType== typeof(MySpecialClass)))
                {
                    //MySpecialClass field
                    var destinationMeasure = new AuditMySpecialClass();

                    var sourceValue = (MySpecialClass)field.GetValue(source);

                    if (sourceValue != null || sourceValue.IsEmpty())
                    {
                        destinationMeasure.UnitOfMeasure = sourceValue.UnitOfMeasure;
                        destinationMeasure.Value = sourceValue.Value;
                    }

                    destinationField.SetValue(destination, destinationMeasure);
                }
            }
        }
Firenter
  • 301
  • 3
  • 14
  • I've created [this issue](https://github.com/thepirat000/Audit.NET/issues/269) to track the change to allow ignoring property matching on certain types, instead of context-wide. – thepirat000 Jan 20 '20 at 18:15