Search code examples
c#aspnetboilerplate

Storing a property value into another table


I am looking for a way to copy the value of some properties of a class to another table in DB. Is there any particular way that I can mark properties of a class as one which their values need to be stored in 2 places (one as part of its own domain object and the other in another domain object, say Summary)?

I a having a Audited entity as below:

public class Audited 
{
    public virtual int Id{ get; set; }

    public virtual string FieldName{ get; set; }

    public virtual string FieldValue{ get; set; }
}

And other entities such as:

public class Plan : FullAuditedEntity
{
    [ToBeAudited]
    public virtual int PlanName{ get; set; }

    public DateTime Date { get; set; }
}

I am looking for a way to be able to mark properties of classes with an attribute (eg. [ToBeAudited]) so that property value gets copied into the Audited table in on insert or update

I have added the [Audited] (Abp.Auditing) to PlanName and getting the following error in log:

ERROR 2018-04-20 10:02:09,286 [5 ] Mvc.ExceptionHandling.AbpExceptionFilter - Object reference not set to an instance of an object. System.NullReferenceException: Object reference not set to an instance of an object. at Abp.EntityHistory.EntityHistoryHelper.ShouldSavePropertyHistory(PropertyEntry propertyEntry, Boolean defaultValue) at Abp.EntityHistory.EntityHistoryHelper.GetPropertyChanges(EntityEntry entityEntry) at Abp.EntityHistory.EntityHistoryHelper.CreateEntityChangeInfo(EntityEntry entityEntry) at Abp.EntityHistory.EntityHistoryHelper.CreateEntityChangeSet(ICollection1 entityEntries) at Abp.Zero.EntityFrameworkCore.AbpZeroCommonDbContext3.d__98.MoveNext() --- End of stack trace from previous location where exception was thrown --- at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at Abp.EntityFrameworkCore.Uow.EfCoreUnitOfWork.d__20.MoveNext() --- End of stack trace from previous location where exception was thrown --- at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at Abp.EntityFrameworkCore.Uow.EfCoreUnitOfWork.d__12.MoveNext() --- End of stack trace from previous location where exception was thrown --- at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at Abp.EntityFrameworkCore.Uow.EfCoreUnitOfWork.d__14.MoveNext() --- End of stack trace from previous location where exception was thrown --- at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at Abp.Domain.Uow.UnitOfWorkBase.d__57.MoveNext() --- End of stack trace from previous location where exception was thrown --- at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at Abp.AspNetCore.Mvc.Uow.AbpUowActionFilter.d__4.MoveNext() --- End of stack trace from previous location where exception was thrown --- at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.d__10.MoveNext() --- End of stack trace from previous location where exception was thrown --- at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.Rethrow(ActionExecutedContext context) at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted) at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.d__14.MoveNext() --- End of stack trace from previous location where exception was thrown --- at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.d__23.MoveNext()


Solution

  • You can leverage on ABP's Entity History feature (which already works without this customization).

    This answer assumes Audited is an Entity to easily use IRepository, but it doesn't have to be:

    public class Audited : Entity
    {
        public virtual int EntityId { get; set; }
    
        public virtual string FieldName { get; set; }
    
        public virtual string FieldValue { get; set; }
    }
    

    First, implement IEntityHistoryStore:

    public class MyEntityHistoryStore : IEntityHistoryStore
    {
        private readonly IRepository<Audited> _auditedRepository;
    
        public MyEntityHistoryStore(IRepository<Audited> auditedRepository)
        {
            _auditedRepository = auditedRepository;
        }
    
        public async Task SaveAsync(EntityChangeSet entityChangeSet)
        {
            foreach (var entityChange in entityChangeSet.EntityChanges)
            {
                var entityType = entityChange.EntityEntry.As<EntityEntry>().Entity.GetType();
    
                foreach (var propertyChange in entityChange.PropertyChanges)
                {
                    var property = entityType.GetProperty(propertyChange.PropertyName);
                    if (property.IsDefined(typeof(ToBeAuditedAttribute)))
                    {
                        await _auditedRepository.InsertAsync(new Audited
                        {
                            EntityId = JsonConvert.DeserializeObject<int>(entityChange.EntityId),
                            FieldName = propertyChange.PropertyName,
                            FieldValue = propertyChange.NewValue
                        });
                    }
                }
            }
        }
    }
    

    Next, replace the service and add to Selectors in the PreInitialize method of your module:

    // using Abp.Configuration.Startup;
    
    Configuration.ReplaceService<IEntityHistoryStore, MyEntityHistoryStore>();
    Configuration.EntityHistory.Selectors.Add(
        new NamedTypeSelector(
            "ToBeAuditedEntities",
            type => type.GetProperties().Any(p => p.IsDefined(typeof(ToBeAuditedAttribute)))
        )
    );
    

    Then, just Insert as usual:

    _planRepository.Insert(new Plan
    {
        PlanName = 42
    });
    

    Audited table:

    Auditeds