ذخیرهی سوابق کامل تغییرات یک رکورد در یک فیلد توسط Entity framework Core
نویسنده: فرشاد داودی
تاریخ: ۱۳۹۹/۰۹/۰۵ ۱۲:۱۵
آدرس: www.dntips.ir
| مطالب | ۳۶۹۴ |
| نویسندگان | ۲۷۶ |
| گروههای مطالب | ۱۰۲۴ |
| نقشههای راه | ۱۱۹ |
| دورهها | ۱۴ |
| اشتراکها | ۱۷۹۱۴ |
public class BaseEntity : IBaseEntity
{
[JsonIgnore]
int Id { get; set; }
[JsonIgnore]
string? Audit { get; set; }
} public class AuditSourceValues
{
[JsonProperty("hn")]
public string? HostName { get; set; }
[JsonProperty("mn")]
public string? MachineName { get; set; }
[JsonProperty("rip")]
public string? RemoteIpAddress { get; set; }
[JsonProperty("lip")]
public string? LocalIpAddress { get; set; }
[JsonProperty("ua")]
public string? UserAgent { get; set; }
[JsonProperty("an")]
public string? ApplicationName { get; set; }
[JsonProperty("av")]
public string? ApplicationVersion { get; set; }
[JsonProperty("cn")]
public string? ClientName { get; set; }
[JsonProperty("cv")]
public string? ClientVersion { get; set; }
[JsonProperty("o")]
public string? Other { get; set; }
} public class EntityAudit<TEntity>
{
[JsonProperty("type")]
[JsonConverter(typeof(StringEnumConverter))]
public EntityEventType EventType { get; set; }
[JsonProperty("user", NullValueHandling = NullValueHandling.Include)]
public int? ActorUserId { get; set; }
[JsonProperty("at")]
public DateTime ActDateTime { get; set; }
[JsonProperty("sources")]
public AuditSourceValues? AuditSourceValues { get; set; }
[JsonProperty("newValues", NullValueHandling = NullValueHandling.Include)]
public TEntity NewEntity { get; set; } = default!;
public string? SerializeJson()
{
return JsonSerializer.Serialize(this,
options: new JsonSerializerOptions { WriteIndented = false, IgnoreNullValues = true });
}
} دقت کنید که این کلاس به صورت جنریک تعریف شده است تا اگر بعدا بخواهیم آن را Deserialize کنیم و مثلا از آن API بسازیم، یا استفادهی خاصی را از آن داشته باشیم، بهراحتی به Entity مد نظر تبدیل شود. در این مقاله فقط به ذخیرهی آن پرداخته میشود و استفاده از این فیلد که به راحتی و با کمک DbFunctionها در Entity Framework قابل انجام است به خواننده واگذار میشود.
public enum EntityEventType
{
Create = 0,
Update = 1,
Delete = 2
} public interface IAuditSourcesProvider
{
AuditSourceValues GetAuditSourceValues();
} public class AuditSourcesProvider : IAuditSourcesProvider
{
protected readonly IHttpContextAccessor HttpContextAccessor;
public AuditSourcesProvider(IHttpContextAccessor httpContextAccessor)
{
HttpContextAccessor = httpContextAccessor;
}
public virtual AuditSourceValues GetAuditSourceValues()
{
var httpContext = HttpContextAccessor.HttpContext;
return new AuditSourceValues
{
HostName = GetHostName(httpContext),
MachineName = GetComputerName(httpContext),
LocalIpAddress = GetLocalIpAddress(httpContext),
RemoteIpAddress = GetRemoteIpAddress(httpContext),
UserAgent = GetUserAgent(httpContext),
ApplicationName = GetApplicationName(httpContext),
ClientName = GetClientName(httpContext),
ClientVersion = GetClientVersion(httpContext),
ApplicationVersion = GetApplicationVersion(httpContext),
Other = GetOther(httpContext)
};
}
protected virtual string? GetUserAgent(HttpContext httpContext)
{
return httpContext.Request?.Headers["User-Agent"].ToString();
}
protected virtual string? GetRemoteIpAddress(HttpContext httpContext)
{
return httpContext.Connection?.RemoteIpAddress?.ToString();
}
protected virtual string? GetLocalIpAddress(HttpContext httpContext)
{
return httpContext.Connection?.LocalIpAddress?.ToString();
}
protected virtual string GetHostName(HttpContext httpContext)
{
return httpContext.Request.Host.ToString();
}
protected virtual string GetComputerName(HttpContext httpContext)
{
return Environment.MachineName;
}
protected virtual string? GetApplicationName(HttpContext httpContext)
{
return Assembly.GetEntryAssembly()?.GetName().Name;
}
protected virtual string? GetApplicationVersion(HttpContext httpContext)
{
return Assembly.GetEntryAssembly()?.GetName().Version.ToString();
}
protected virtual string? GetClientVersion(HttpContext httpContext)
{
return httpContext.Request?.Headers["client-version"];
}
protected virtual string? GetClientName(HttpContext httpContext)
{
return httpContext.Request?.Headers["client-name"];
}
protected virtual string? GetOther(HttpContext httpContext)
{
return null;
}
} public interface IEntityAuditProvider
{
string? GetAuditValues(EntityEventType eventType, object? entity, string? previousJsonAudit = null);
} public class EntityAuditProvider : IEntityAuditProvider
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IAuditSourcesProvider _auditSourcesProvider;
#region Constructor Injections
public EntityAuditProvider(IHttpContextAccessor httpContextAccessor, IAuditSourcesProvider auditSourcesProvider)
{
_httpContextAccessor = httpContextAccessor;
_auditSourcesProvider = auditSourcesProvider;
}
#endregion
public virtual string? GetAuditValues(EntityEventType eventType, object? newEntity, string? previousJsonAudit = null)
{
var httpContext = _httpContextAccessor.HttpContext;
int? userId;
var user = httpContext.User;
if (!user.Identity.IsAuthenticated)
userId = null;
else
userId = user.Claims.Where(x => x.Type == "UserID").Select(x => x.Value).First().ToInt();
var auditSourceValues = _auditSourcesProvider.GetAuditSourceValues();
var auditJArray = new JArray();
// Update & Delete
if (eventType == EntityEventType.Update || eventType == EntityEventType.Delete)
{
auditJArray = JArray.Parse(previousJsonAudit!);
}
// Delete => No NewValues
if (eventType == EntityEventType.Delete)
{
newEntity = null;
}
JObject newAuditJObject = JObject.FromObject(new EntityAudit<object?>
{
EventType = eventType,
ActorUserId = userId,
ActDateTime = DateTime.Now,
AuditSourceValues = auditSourceValues,
NewEntity = newEntity
}, new JsonSerializer
{
NullValueHandling = NullValueHandling.Ignore,
Formatting = Formatting.None
});
auditJArray.Add(newAuditJObject);
return auditJArray.SerializeToJson(true);
}
} public class AuditSaveChangesInterceptor : SaveChangesInterceptor
{
private readonly IEntityAuditProvider _entityAuditProvider;
#region Constructor Injections
public AuditSaveChangesInterceptor(IEntityAuditProvider entityAuditProvider)
{
_entityAuditProvider = entityAuditProvider;
}
#endregion
public override InterceptionResult<int> SavingChanges(DbContextEventData eventData, InterceptionResult<int> result)
{
ApplyAudits(eventData.Context.ChangeTracker);
return base.SavingChanges(eventData, result);
}
public override ValueTask<InterceptionResult<int>> SavingChangesAsync(DbContextEventData eventData, InterceptionResult<int> result,
CancellationToken cancellationToken = new CancellationToken())
{
ApplyAudits(eventData.Context.ChangeTracker);
return base.SavingChangesAsync(eventData, result, cancellationToken);
}
private void ApplyAudits(ChangeTracker changeTracker)
{
ApplyCreateAudits(changeTracker);
ApplyUpdateAudits(changeTracker);
ApplyDeleteAudits(changeTracker);
}
private void ApplyCreateAudits(ChangeTracker changeTracker)
{
var addedEntries = changeTracker.Entries()
.Where(x => x.State == EntityState.Added);
foreach (var addedEntry in addedEntries)
{
if (addedEntry.Entity is IBaseEntity entity)
{
entity.Audit = _entityAuditProvider.GetAuditValues(EntityEventType.Create, entity);
}
}
}
private void ApplyUpdateAudits(ChangeTracker changeTracker)
{
var modifiedEntries = changeTracker.Entries()
.Where(x => x.State == EntityState.Modified);
foreach (var modifiedEntry in modifiedEntries)
{
if (modifiedEntry.Entity is IBaseEntity entity)
{
var eventType = entity.IsArchived ? EntityEventType.Delete : EntityEventType.Update; // Maybe Soft Delete
entity.Audit = _entityAuditProvider.GetAuditValues(eventType, entity, entity.Audit);
}
}
}
private void ApplyDeleteAudits(ChangeTracker changeTracker)
{
var deletedEntries = changeTracker.Entries()
.Where(x => x.State == EntityState.Deleted);
foreach (var modifiedEntry in deletedEntries)
{
if (modifiedEntry.Entity is IBaseEntity entity)
{
entity.Audit = _entityAuditProvider.GetAuditValues(EntityEventType.Delete, entity, entity.Audit);
}
}
}
}
و سپس آن را به سیستم معرفی میکنیم:
services.AddDbContext<ATADbContext>((serviceProvider, options) =>
{
options
.UseSqlServer(...)
// Interceptors
var entityAuditProvider = serviceProvider.GetRequiredService<IEntityAuditProvider>();
options.AddInterceptors(new AuditSaveChangesInterceptor(entityAuditProvider));
});
[
{
"type":"Create",
"user":1,
"at":"2020-11-24T23:05:54.2692711+03:30",
"sources":{
"hn":"localhost:44398",
"mn":"DESKTOP-N1GAV2U",
"rip":"::1",
"lip":"::1",
"ua":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36",
"an":"Server.Api",
"av":"1.0.0.0"
},
"newValues":{
"Name":"Farshad"
}
},
{
"type":"Update",
"user":1,
"at":"2020-11-24T23:06:20.0838188+03:30",
"sources":{
"hn":"localhost:44398",
"mn":"DESKTOP-N1GAV2U",
"rip":"::1",
"lip":"::1",
"ua":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36",
"an":"Server.Api",
"av":"1.0.0.0"
},
"newValues":{
"Name":"Edited Farshad"
}
},
{
"type":"Delete",
"user":null,
"at":"2020-11-24T23:06:28.601837+03:30",
"sources":{
"hn":"localhost:44398",
"mn":"DESKTOP-N1GAV2U",
"rip":"::1",
"lip":"::1",
"ua":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36",
"an":"Server.Api",
"av":"1.0.0.0"
},
"newValues":null
}
]
ولی روش گفته شده در این مقاله، همین عملیات را به صورت کاملتری و فقط بر روی یک ستون همان جدول انجام میدهد که باعث ذخیرهی دیتای کمتر، یکپارچگی بهتر و دسترسیپذیری و راحتی استفاده از آن میشود.