zzzprojects / EntityFramework-Plus

Entity Framework Plus extends your DbContext with must-haves features: Include Filter, Auditing, Caching, Query Future, Batch Delete, Batch Update, and more
https://entityframework-plus.net/
MIT License
2.27k stars 318 forks source link

Changesets of AuditEntries #89

Open dmeierotto opened 8 years ago

dmeierotto commented 8 years ago

I would suggest a feature of changesets for your audits. This would create a Changeset Id once per save. This would make it easier to see all changes related to a certain entity(e.g. if I save a whole graph, I can just display all changes in the changeset where a given property was affected).

Thanks again for producing a great library.

zzzprojects commented 8 years ago

Thank you, this is an excellent idea and without a doubt very useful.

This will for sure be implemented on the next major version of the Audit Feature.

zzzprojects commented 7 years ago

Hello @dmeierotto ,

I forget to say you could already implement it yourself by using custom class See: https://github.com/zzzprojects/EntityFramework-Plus/wiki/EF-Audit-%7C-Entity-Framework-Audit-Trail-Context-and-Track-Changes#audit-customization

Let me know if you need some help to implement it.

As said, this is for sure a suggestion we keep in mind to include by default in the next major version.

Best Regards,

Jonathan

quang-nguyen commented 7 years ago

Hello,

Could you please give an example how to implement it? I'm having issue. I currently have AuditEntry and AuditEntryProperty tables in my db (code first). I added new custom classes like this:

public class CustomAuditEntry : AuditEntry { public Guid Changeset { get; set; } } public class CustomAuditEntryProperty : AuditEntryProperty { public Guid Changeset { get; set; } }

When i add new migration, it adds AuditEntry table again, and it creates table CustomAuditEntry which only has Changeset column? Should CustomAuditEntry table has all properties of AuditEntry table plus Changeset column?

Here is the snippet of migration script: CreateTable( "Core.AuditEntry", c => new { AuditEntryID = c.Int(nullable: false, identity: true), EntitySetName = c.String(maxLength: 255), EntityTypeName = c.String(maxLength: 255), State = c.Int(nullable: false), StateName = c.String(maxLength: 255), CreatedBy = c.String(maxLength: 255), CreatedDate = c.DateTime(nullable: false), }) .PrimaryKey(t => t.AuditEntryID);

`CreateTable(
    "Core.CustomAuditEntry",
    c => new
        {
            AuditEntryID = c.Int(nullable: false),
            Changeset = c.Guid(nullable: false),
        })
    .PrimaryKey(t => t.AuditEntryID)
    .ForeignKey("Core.AuditEntry", t => t.AuditEntryID)
    .Index(t => t.AuditEntryID);`

`CreateTable(
    "Core.CustomAuditEntryProperty",
    c => new
        {
            AuditEntryPropertyID = c.Int(nullable: false),
            Changeset = c.Guid(nullable: false),
        })
    .PrimaryKey(t => t.AuditEntryPropertyID)
    .ForeignKey("Core.AuditEntryProperty", t => t.AuditEntryPropertyID)
    .Index(t => t.AuditEntryPropertyID);

DropTable("Core.AuditEntry");`

Then I did update-database i got error because AuditEntry already exist. Am I doing something wrong?

JonathanMagnan commented 7 years ago

Hello @quang-nguyen ,

Please try to create your own issue topic next time, it's easier to follow request this way.

Then I did update-database i got error because AuditEntry already exist. Am I doing something wrong?

This one seems more a migration error, so I'm not sure what to answer. If your database is new, simply delete everything.

When i add new migration, it adds AuditEntry table again, and it creates table CustomAuditEntry which only has Changeset column

Yes, by default it's normal 4 audit tables is created. However, you can change the mapping to use instead TPH so only two table will be created. See the example below which use TPH:

using System.Data.Entity;
using System.Windows.Forms;

namespace Z.EntityFramework.Plus.Lab
{
    public partial class Form_Request_Audit_CustomAudit : Form
    {
        public Form_Request_Audit_CustomAudit()
        {
            InitializeComponent();

            AuditManager.DefaultConfiguration.AutoSavePreAction = (context, audit) =>
                (context as EntityContext).AuditEntries.AddRange(audit.Entries);

            AuditManager.DefaultConfiguration.AuditEntryFactory = args =>
                new CustomAuditEntry {CustomProperty = 1};

            AuditManager.DefaultConfiguration.AuditEntryPropertyFactory = args =>
                new CustomAuditEntryProperty {CustomProperty = 2};

            using (var ctx = new EntityContext())
            {
                var audits = new Audit();
                audits.CreatedBy = "ZZZ Projects"; // Optional

                ctx.EntitySimples.Add(new EntitySimple {ColumnInt = 2});
                ctx.SaveChanges(audits);
            }
        }

        public class EntityContext : DbContext
        {
            public EntityContext() : base("CodeFirstEntities")
            {
            }

            // ... context code ...
            public DbSet<AuditEntry> AuditEntries { get; set; }
            public DbSet<AuditEntryProperty> AuditEntryProperties { get; set; }
            public DbSet<EntitySimple> EntitySimples { get; set; }

            protected override void OnModelCreating(DbModelBuilder modelBuilder)
            {
                // INTERNAL MAPPING: ensure unique name...
                modelBuilder.Entity<EntitySimple>().ToTable(GetType().DeclaringType.FullName.Replace(".", "_") + "_" + typeof(EntitySimple).Name);
                modelBuilder.Entity<AuditEntry>().ToTable(GetType().DeclaringType.FullName.Replace(".", "_") + "_" + typeof(AuditEntry).Name);
                modelBuilder.Entity<AuditEntryProperty>().ToTable(GetType().DeclaringType.FullName.Replace(".", "_") + "_" + typeof(AuditEntryProperty).Name);

                // MAP audit using TPH
                modelBuilder.Entity<AuditEntry>().Map<CustomAuditEntry>(x => x.Requires("Discriminator").HasValue("CustomAuditEntry"));
                modelBuilder.Entity<AuditEntryProperty>().Map<CustomAuditEntryProperty>(x => x.Requires("Discriminator").HasValue("CustomAuditEntryProperty"));

                base.OnModelCreating(modelBuilder);
            }
        }
    }

    public class EntitySimple
    {
        public int ID { get; set; }
        public int ColumnInt { get; set; }
    }

    public class CustomAuditEntry : AuditEntry
    {
        public int CustomProperty { get; set; }
    }

    public class CustomAuditEntryProperty : AuditEntryProperty
    {
        public int CustomProperty { get; set; }
    }
}