Base solution for your next web application
Open Closed

Capturing all changes to entities #12618


User avatar
0
KieranIrl created

Hi,

We're aware of the AbpAuditLogs table and the parameters column that captures the payload for post requests and how it's limited to 1024 characters approximately. We also know about the AbpEntityPropertyChanges table that captures the new and old values (512 characters)

We have many forms that could have 10 rich text text editors with nvarchar(max) . We'd like to track how these properties change over time.

If we wanted to capture all changes without losing information due to column size restriction , what approach would you suggest?

Our framework version is about 2 years old for reference but I'm not sure if this would affect your answer.

Markdown is supported
Copy & paste or drag & drop images (max 30 MB per image)

3 Answer(s)
  • User Avatar
    0
    oguzhanagir created
    Support Team

    Hi @KieranIrl

    Recommended Approaches

    Approach 1: Custom Entity History Store (Recommended)

    You can fully customize how entity history is stored by implementing ABP’s IEntityHistoryStore interface and persisting large values in your own table.

    // Core layer
    public class ExtendedEntityPropertyChange : Entity<long>
    {
        public long EntityChangeId { get; set; }
        public string PropertyName { get; set; }
        public string PropertyTypeFullName { get; set; }
    
        // Store as nvarchar(max)
        public string OriginalValue { get; set; }
        public string NewValue { get; set; }
    
        public int? TenantId { get; set; }
    }
    
    // EntityFrameworkCore layer
    public class CustomEntityHistoryStore : IEntityHistoryStore, ITransientDependency
    {
        private readonly IRepository<ExtendedEntityPropertyChange, long> _extendedPropertyChangeRepository;
        private readonly IRepository<EntityChangeSet, long> _changeSetRepository;
    
        public async Task SaveAsync(EntityChangeSet changeSet)
        {
            // Save default change set
            await _changeSetRepository.InsertAsync(changeSet);
    
            // Persist large values separately
            foreach (var entityChange in changeSet.EntityChanges)
            {
                foreach (var propertyChange in entityChange.PropertyChanges)
                {
                    await _extendedPropertyChangeRepository.InsertAsync(
                        new ExtendedEntityPropertyChange
                        {
                            EntityChangeId = entityChange.Id,
                            PropertyName = propertyChange.PropertyName,
                            PropertyTypeFullName = propertyChange.PropertyTypeFullName,
                            OriginalValue = propertyChange.OriginalValue,
                            NewValue = propertyChange.NewValue,
                            TenantId = propertyChange.TenantId
                        });
                }
            }
        }
    }
    

    Module registration:

    public override void PreInitialize()
    {
        Configuration.ReplaceService<IEntityHistoryStore, CustomEntityHistoryStore>();
    }
    

    Approach 2: Extending Database Column Limits

    You can modify the existing ABP tables via migration to support nvarchar(max).

    public partial class ExtendEntityPropertyChangeColumns : Migration
    {
        protected override void Up(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.AlterColumn<string>(
                name: "NewValue",
                table: "AbpEntityPropertyChanges",
                type: "nvarchar(max)",
                nullable: true);
    
            migrationBuilder.AlterColumn<string>(
                name: "OriginalValue",
                table: "AbpEntityPropertyChanges",
                type: "nvarchar(max)",
                nullable: true);
        }
    }
    

    You must also override the entity configuration using Fluent API:

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<EntityPropertyChange>(b =>
        {
            b.Property(e => e.NewValue).HasMaxLength(int.MaxValue);
            b.Property(e => e.OriginalValue).HasMaxLength(int.MaxValue);
        });
    }
    

    Be careful during future ABP upgrades, as framework migrations may override these changes.

    Approach 3: Custom Auditing via Domain Events

    For selective auditing (only large or sensitive fields), a domain event based approach is often the cleanest solution.

    public class MyDocument : FullAuditedEntity<long>
    {
        public string RichTextContent1 { get; set; }
        public string RichTextContent2 { get; set; }
        // ... up to 10 rich text fields
    }
    
    public class DocumentContentChangedEvent : IEventData
    {
        public long DocumentId { get; set; }
        public string PropertyName { get; set; }
        public string OldValue { get; set; }
        public string NewValue { get; set; }
        public DateTime ChangeTime { get; set; }
        public long? UserId { get; set; }
    }
    
    public class DocumentAuditEventHandler : IEventHandler<DocumentContentChangedEvent>
    {
        private readonly IRepository<DocumentAuditLog, long> _auditLogRepository;
    
        public void HandleEvent(DocumentContentChangedEvent eventData)
        {
            _auditLogRepository.Insert(new DocumentAuditLog
            {
                DocumentId = eventData.DocumentId,
                PropertyName = eventData.PropertyName,
                OldValue = eventData.OldValue,
                NewValue = eventData.NewValue,
                ChangeTime = eventData.ChangeTime,
                UserId = eventData.UserId
            });
        }
    }
    

    Approach 4: Hybrid Solution with Blob Storage

    For very large content (e.g., 50KB+ HTML), you can store values externally (Azure Blob, S3, or DB BLOBs) and keep only references in audit tables.

    public class ExtendedPropertyChange : Entity<long>
    {
        public long EntityChangeId { get; set; }
        public string PropertyName { get; set; }
    
        // Short values
        public string NewValueShort { get; set; }
        public string OriginalValueShort { get; set; }
    
        // Large content references
        public Guid? NewValueBlobId { get; set; }
        public Guid? OriginalValueBlobId { get; set; }
    }
    

    Framework Version Considerations

    Using an ABP version that is ~2 years old does not materially affect these solutions:

    1. IEntityHistoryStore has existed since ABP 3.x
    2. EF Core Fluent API works across all supported versions
    3. Domain Events are also available in older versions

    Just ensure you verify the actual column limits in your current database schema and migration history.

    • If only specific entities or fields require large text tracking → Approach 3 (Domain Events)
    • If you need a system wide solution → Approach 1 (Custom EntityHistoryStore)

    This provides the best balance between maintainability, scalability, and framework compatibility.

    If you need any further information or clarification, please do not hesitate to contact us.

    Thank you.

    Markdown is supported
    Copy & paste or drag & drop images (max 30 MB per image)
  • User Avatar
    0
    KieranIrl created

    Thanks for the detailed response.

    Markdown is supported
    Copy & paste or drag & drop images (max 30 MB per image)
  • User Avatar
    0
    oguzhanagir created
    Support Team

    We're glad we could help. If you have any further questions, please don't hesitate to contact us. Thank you.

    Markdown is supported
    Copy & paste or drag & drop images (max 30 MB per image)