Goal is to track who had changed and deleted an entity.
So I have an entity that implements an interface:
interface IAuditable {
string ModifiedBy {get;set;}
}
class User: IAuditable {
public int UserId {get;set;}
public string UserName {get;set;}
public string ModifiedBy {get;set;}
[Timestamp]
public byte[] RowVersion { get; set; }
}
Now the code of entity remove operation could look like this :
User user = context.Users.First();
user.ModifiedBy = CurrentUser.Name;
context.Users.Remove(employer);
context.SaveContext();
In real: ModifiedBy
update will be never executed (when my db history triggers expect to "handle" it). Only delete statement will be executed on DB.
I want to know how to force EF Core "update" deleted entities/entries (which implements the specific interface) if entity was modified.
Note: RowVersion
adds additional complexity.
P.S. To put additional SaveContext call manually - of course is an option, but I would like to have a generic solution: many various updates and deletes, then one SaveContext do all analyzes.
To update those properties manually before SaveContext collecting var deletedEntries = entries.Where(e => e.State == EntityState.Deleted && isAuditable(e))
it is not an option since it can ruin EF Core locks order management and therefore provoke deadlocks.
Most clear solution would be just stay with one SaveContext call but inject UPDATE statement on auditable fields just before EF CORE
call DELETE
. How to achieve this? May be somebody has the solution already?
Alternative could be "on delete do not compose DELETE statement but call stored procedure that can accept auditable fields as paramaters"
Interesting question. At the time of writing (EF Core 2.1.3), there is no such public API. The following solution is based on the internal APIs, which in EF Core fortunately are publicly accessible under the typical internal API disclaimer:
Now the solution. The service responsible for modification command creation is called
ICommandBatchPreparer
:It contains a single method called
BatchCommands
:with the following signature:
and default implementation in
CommandBatchPreparer
class.We will replace that service with custom implementation which will extend the list with "modified" entries and use the base implementation to do the actual job. Since batch is basically a lists of modification commands sorted by dependency and then by type with
Delete
being beforeUpdate
, we will use separate batch(es) for the update commands first and concatenate the rest after.The generated modification commands are based on
IUpdateEntry
:Luckily it's an interface, so we will provide our own implementation for the additional "modified" entries, as well as for their corresponding delete entries (more on that later).
First we'll create a base implementation which simply delegates the calls to the underlying object, thus allowing us to override later only the methods that are essential for what we are trying to achieve:
Now the first custom entry:
First we "modify" the source state from
Deleted
toModified
. Then we modify theIsModified
method which returnsfalse
forDeleted
entries to returntrue
for the auditable properties, thus forcing them to be included in the update command. Finally we modify theIsStoreGenerated
method which also returnsfalse
forDeleted
entries to return the corresponding result for theModified
entries (EF Core code). This is needed to let EF Core correctly handle the database generated values on update likeRowVersion
. After executing the command, EF Core will callSetCurrentValue
with the values returned from the database. Which does not happen for normalDeleted
entries and for normalModified
entries propagates to their entity.Which leads us to the need of the second custom entry, which will wrap the original entry and also will be used as source for the
AuditUpdateEntry
, hence will receive theSetCurrentValue
from it. It will store the received values internally, thus keeping the original entity state unchanged, and will treat them as both "current" and "original". This is essential because the delete command will be executed after update, and if theRowVersion
does not return the new value as "original", the generated delete command will fail.Here is the implementation:
With these two custom entries we are ready to implement our custom command batch builder:
and we are almost done. Add a helper method for registering our service(s):
and call it from you
DbContext
derived classOnConfiguring
override:and you are done.
All this is for single auditable field populated manually just to get the idea. It can be extended with more auditable fields, registering a custom auditable fields provider service and automatically filling the values for insert/update/delete operations etc.
P.S. Full code
Update: Full code updated for EF Core 5 (not tested):