3

I'm trying to implement the Audit.EntityFramework.Core package from the Audit.Net repository but am running into some difficulty. I'm unable to save changes or target a different database. I've modified my SaveChanges and SaveChangesAsync function to call the Audit.Net DbContextHelper class's save functions but I'm missing something.

Is there a way to do the following?

  1. Target another database for storing audit data using an audit DbContext that inherits from the DbContext I'm trying to audit?
    public class MyDbContext : DbContext {} //Types defined here
    public class AuditDbContext : MyDbContext {} //This context stores audit data into a different DB
    
  2. Not require mapping between the type and its audited type when setting up a global connection? (I'm trying to avoid calling AuditTypeMapper explicitly for each type with a model that's currently undergoing a lot of change).
    //MyDbContext has different connection string than AuditDbContext
    Audit.Core.Configuration.Setup()
    .UseEntityFramework(x => x
        .UseDbContext<AuditDbContext>());
    

I've tried code that resembles the following but get runtime errors on SaveChanges that indicate that there is no model set up. Adding a migration for the AuditDbContext didn't help.

Josh
  • 2,422
  • 2
  • 27
  • 18
  • You are doing it right, but you need to define a mapping from your entity types to your audit entity types. How is it going to be? How does your audit tables looks like? – thepirat000 Sep 04 '19 at 17:44
  • Thanks for your response. I was trying to do an audit table per type, hopefully using inheritance to create a set of audit tables based on the original objects. But I'm open to using a single audit table if the other way will require too much upkeep. – Josh Sep 04 '19 at 18:04
  • Please check [EF Provider configuration examples](https://github.com/thepirat000/Audit.NET/tree/master/src/Audit.EntityFramework#ef-provider-configuration-examples). You have many options. Are your entities and audit entities defined on the same assembly and namespace? – thepirat000 Sep 04 '19 at 19:49
  • I have defined the audit entities on the same namespace as the base entities. I'll go through those docs you referenced again and post when I get a satisfactory solution. Thanks. – Josh Sep 04 '19 at 21:23

1 Answers1

1

I figured out what I was trying to do.

My design goals were:

  1. Store audit records in a different database
  2. Have an audit table per type that matches the audited type (with additional audit fields)
  3. Require no upkeep of separate audit entities. Changes between operational DB and audit DB should be seamless

Things I discovered that did not work were:

  1. Creating an audit DbContext that inherited from my operational DbContext doesn't work because the relationships, DBSets, and ID's could not be treated the same way in an audit DB.
  2. Dynamically creating types using reflection over operational types with TypeBuilder doesn't work because Audit.Net casts objects between their operational and audit types and casting from a CLR type to a dynamically created type fails.
  3. Mixing concrete types and EF Core shadow types doesn't work.

Steps Taken

  1. Set up Global Auditing (in main setup code)

    //Global setup of Auditing
    var auditDbCtxOptions = new DbContextOptionsBuilder<MyAuditDbContext>()
        .UseSqlServer(options.AuditDbConnectionString)
        .Options;
    
    Audit.Core.Configuration.Setup()
        .UseEntityFramework(x => x
            .UseDbContext<MyAuditDbContext>(auditDbCtxOptions)
            .AuditTypeNameMapper(typeName => 
            {
                return typeName;
            })
            .AuditEntityAction<AuditInfo>((ev, ent, auditEntity) =>
            {
                auditEntity.DatabaseAction = ent.Action;
            }));
    
  2. Had my audited models inherit from a baseclass AuditInfo

    public abstract class AuditInfo
    {
        public DateTime Created { get; set; }
        public DateTime? Updated { get; set; }
        public string CreatedBy { get; set; }
        public string UpdatedBy { get; set; }
    
        [NotMapped] //This is not mapped on the operational DB
        public string DatabaseAction { get; set; }
    }
    
  3. Created a reflection-based audit schema using a new DbContext and OnModelCreating

    public class MyAuditContext : DbContext
    {
        public MyAuditContext(DbContextOptions<MyAuditContext> options) : base(options)
        {
    
        }
    
        private readonly Type[] AllowedTypes = new Type[] 
        { 
            typeof(bool),
            typeof(int),
            typeof(decimal),
            typeof(string),
            typeof(DateTime),
        };
    
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            Console.WriteLine($"Generating dynamic audit model");
    
            //Go through each of the types in Hsa.Engine.Data.Models
            var asm = Assembly.GetExecutingAssembly();
            var modelTypes = asm.GetTypes()
                .Where(type => type.Namespace == "My.Data.Models.Namespace");
    
            //Create an entity For each type get all the properties on the model
            foreach(var model in modelTypes.Where(t => t.IsClass && !t.IsAbstract && t.BaseType == typeof(AuditInfo)))
            {
                Console.WriteLine($"Creating entity for {model.Name}");
    
                var table = modelBuilder.Entity(model, entity => 
                {
                    //Remove all types from base model, otherwise we get a bunch of noise about foreign keys, etc.
                    foreach(var prop in model.GetProperties())
                    {
                        entity.Ignore(prop.Name);
                    }
    
                    foreach(var prop in model.GetProperties().Where(p => AllowedTypes.Any(t => p.PropertyType.IsAssignableFrom(t))))
                    {
                        Console.WriteLine($"   Adding field: {prop.Name} - Type: {prop.PropertyType.Name}");
                        //Create a typed field for each property, not including ID or foreign key annotations (do include field lengths)
    
                        var dbField = entity.Property(prop.PropertyType, prop.Name);
    
                        if(prop.PropertyType.IsEnum)
                        {
                            dbField.HasConversion<string>();
                        }
    
                        if(dbField.Metadata.IsPrimaryKey())
                        {
                            dbField.ValueGeneratedNever(); //Removes existing model primary keys for the audit DB
                        }
                    }
    
                    //Add audit properties
                    entity.Property<int>("AuditId").IsRequired().UseSqlServerIdentityColumn();
                    entity.Property<DateTime>("AuditDate").HasDefaultValueSql("getdate()");
                    entity.Property<string>("DatabaseAction"); //included on AuditInfo but NotMapped to avoid putting it on the main DB. Added here to ensure it makes it into the audit DB
    
                    entity.HasKey("AuditId");
                    entity.HasIndex("Id");
                    entity.ToTable("Audit_" + model.Name);
                });
            }
    
            base.OnModelCreating(modelBuilder);
        }
    }
    
    
  4. Created a migration for both the primary DB and the Audit DB.

Some people may not need to go to these levels but I wanted to share in case anyone needed something similar when using Audit.Net

Josh
  • 2,422
  • 2
  • 27
  • 18
  • Hi, and thank you for the interesting suggestions. I'd like to try this approach with a .NET Core app, but there's something I don't fully understand. I'f I'm not wrong your models inherit from the AuditInfo base class, right? But you're not explicitly coding the corresponding audit models, because you're using reflection on the audited models in the MyAuditContext.OnModelCreating. Is this correct? What's unclear is: what about migrations? How do you manage to propagate the changes happening in the audited models to the AuditDbContext? Thanks, Matteo – m.phobos Apr 22 '20 at 08:30
  • Whoa... Blast from the past seeing this three years later. To answer you a little too late, yes migrations do work. I've successfully implemented a shadow-db auditing system in two production systems now. This method works really well and is fast and seamless for the development workflow. – Josh May 02 '23 at 04:12