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(ICollection
1 entityEntries) at Abp.Zero.EntityFrameworkCore.AbpZeroCommonDbContext
3.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()
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: