ایجاد History سفارسی در متد SaveChanges جهت ثبت تغییرات
سلام دوستان
می خواستم تغییرات یک ردیف در یک جدول از بانک اطلاعاتی را بصورت سفارشی و با توجه به نیاز خودم انجام بدم. آنچه که در اینترنت دیدم override کردن متد Savechangegs در کلاس Context بود. که با توجه به State یک Entity اقدام لازم را انجام میداد. روشی که خودم میخوام پیاده کنم به این صورت هستش:
- یک جدول با نام History جهت ثبت ایجاد کردم که شامل UserId, EntityFullName, ReferenceId, DataAsJson, State هستش.
- تمام جداول دارای ستون هایی جهت ثبت تاریخ CreateDate, UpdateDate و DeleteDate هستند.
- اگر یک ردیف توسط کاربر Delete شود فقط تیک گزینه IsDeleted فعال می شود و دیگر در دسترس نخواهد بود و مقدار DeleteDate نیز بروزرسانی خواهد شد. و در انتهای نام کاربر، نام کامل Entity و شماره ردیف و State در جدول History ثبت خواهد شد.
- اگر ردیف اضافه شود. مطابق روش Delete فعالیت کاربر ثبت خواهد شد.
- اگر یک ردیف ویرایش شود برنامه قبل از ذخیره اطلاعات جدید ابتدا اطلاعات قدیمی را بصورت Json در جدول History ذخیر خواهد کرد و سپس اقدام به بروزرسانی اطلاعات جدید خواهد کرد. هر زمان که تغییرات اطلاعات را بخوام بررسی کنم اطلاعت json را به کلاسی که در ستون EntityFullName ثبت شده تبدیل میکنم و نتیجه را برای کاربر ارسال میکنم که اگر تعداد دفعات تغییر یک ردیف زیاد باشه نتیجه را بصورت یک لیست ارسال میکنم.
دوستان اگر روشی برای کار با تاریخچه تغییرات میدونید راهنمایی کنید. از CDC موجود در SQL Server نمیخوام استفاده کنم.
تشکر
نقل قول: ایجاد History سفارسی در متد SaveChanges جهت ثبت تغییرات
سلام دوستان
کسی با روشی که توضیح دادم و یا مشابه اون کار کرده؟ اگر نظری هم در خصوص این روش دارید ممنون میشم مطرح کنید.
تشکر
نقل قول: ایجاد History سفارسی در متد SaveChanges جهت ثبت تغییرات
سلام
روش AuditFields
که شما گفتید دقیقا همین هست
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default(CancellationToken))
{
AuditFields();
return (await base.SaveChangesAsync(true, cancellationToken));
}
private void AuditFields()
{
var userId = _accessor.HttpContext?.User?.Identity?.GetUserId() ;
var auditUser = _accessor.HttpContext?.User?.Identity?.Name;
var auditDate = DateTime.Now;
var ipAddress = HttpContextExtensionsGetRemoteIP.GetRemoteIPAddres s(_accessor.HttpContext).ToString();
foreach (var entry in this.ChangeTracker.Entries<BaseEntity>())
{
switch (entry.State)
{
case EntityState.Added:
entry.Entity.InsertDate = auditDate;
entry.Entity.InsertBy = auditUser;
entry.Entity.InsertedBy = userId;
entry.Entity.IsActive = true;
entry.Entity.InsertIpAddress = ipAddress;
break;
case EntityState.Modified:
entry.Entity.UpdateDate = auditDate;
entry.Entity.UpdateBy = auditUser;
entry.Entity.UpdatedBy = userId;
entry.Entity.UpdateIpAddress = ipAddress;
break;
}
}
}
من هم همین روش را پیاده سازی کردم
در زمان ایجاد رکورد فیلدهای InsertBy , InsertDate
و هر نوع ویرایش دو تا فیلد UpdateBy , UpdateDate مقدار دهی میشوند.
IsDelete هم شامل ویرایش میشود.
تنها حالتی که بهش فکر کردم ولی هنوز پیاده نکردم در قسمت
case EntityState.Modified:
راه حلهای زیادی رو بررسی کردم ولی این موضوع که کدام فیلد تغییر کرده بهترین روش هست ولی هنوز به نتیجه نرسیده ام.
var changeInfo = context.ChangeTracker.Entries()
.Where (t => t.State == EntityState.Modified)
.Select (t => new {
Original = t.OriginalValues.Properties.ToDictionary (pn => pn, pn => t.OriginalValues[pn]),
Current = t.CurrentValues.Properties.ToDictionary (pn => pn, pn => t.CurrentValues[pn]),
});
برای هر جدول اگر بخواهیم یک جدول سایه ایجاد کنیم مقدار داده ها زیاد میشود و عملا مقایسه هم بی معناست
در واقع داده های تغییر نکرده اهمیتی ندارند و لزوما فیلدهای تغییر داده شده ملاک هستند.
تبدیل همه داده ها به Json و نگهداری نوع کلاس در یک فیلد از نظر من روش مناسبی نیست. منطقی برایش پیدا نکردم و این سوالات به وجود آمد:
هر بار که نیاز به گزارش گیری یا بررسی داریم باید Cast انجام بدیم؟
آیا احتیاج داریم که بدانیم اینها از چه کلاسی مشتق شده اند؟
اگر یک روزی این کلاس تغییر کرد نتیجه داده هایی که داریم چه سرانجامی خواهند داشت؟
و ....
خوشحال میشوم اگر روش مناسبی پیدا کردید بنده را هم در جریان قرار بدهید
نقل قول: ایجاد History سفارسی در متد SaveChanges جهت ثبت تغییرات
تکمیلی مورد قبل:
عذرخواهی میکنم این مورد را کلا فراموش کرده بودم یکی از دوستان پرسید ادامه اش را اینجا میگذارم:
این نسخه کامل شده همون موارد بالایی هست
دو کلاس ایجاد کردم به نام Audit و AuditEntry
public class Audit
{
public long Id { get; set; }
public string CommitValue { get; set; }
public DateTime DateTime { get; set; }
public string UserId { get; set; }
public string UserName { get; set; }
public string Type { get; set; }
public string TableName { get; set; }
public string OldValues { get; set; }
public string NewValues { get; set; }
public string AffectedColumns { get; set; }
public string PrimaryKey { get; set; }
public string IdKey { get; set; }
public string ActionName { get; set; }
public string PCommitValue { get; set; }
public string PersianTableName { get; set; }
public string PAffectedColumns { get; set; }
}
public class AuditEntry
{
public AuditEntry(EntityEntry entry)
{
Entry = entry;
}
public EntityEntry Entry { get; }
public string UserId { get; set; }
public string UserName { get; set; }
public string TableName { get; set; }
public string PersianTableName { get; set; }
public Dictionary<string, object> KeyValues { get; } = new Dictionary<string, object>();
public Dictionary<string, object> OldValues { get; } = new Dictionary<string, object>();
public Dictionary<string, object> NewValues { get; } = new Dictionary<string, object>();
public AuditType AuditType { get; set; }
public List<string> ChangedColumns { get; } = new List<string>();
public List<string> PChangedColumns { get; } = new List<string>();
public Audit ToAudit()
{
var audit = new Audit();
audit.UserId = UserId;
audit.UserName = UserName;
audit.Type = AuditType.ToString();
audit.TableName = TableName;
audit.PersianTableName= PersianTableName;
audit.DateTime = DateTime.Now;
audit.PrimaryKey = JsonSerializer.Serialize(KeyValues);
audit.OldValues = OldValues.Count == 0 ? null : JsonSerializer.Serialize(OldValues);
audit.NewValues = NewValues.Count == 0 ? null : JsonSerializer.Serialize(NewValues);
audit.AffectedColumns = ChangedColumns.Count == 0 ? null : JsonSerializer.Serialize(ChangedColumns);
audit.PAffectedColumns = ChangedColumns.Count == 0 ? null : JsonSerializer.Serialize(PChangedColumns);
audit.IdKey = KeyValues.Values.FirstOrDefault()?.ToString();
foreach (var newItem in NewValues)
{
foreach (var oldItem in OldValues.Where(w=>w.Key == newItem.Key).Where(w=>!object.Equals(w.Value , newItem.Value)))
{
//var xxx = object.ReferenceEquals(oldItem.Value, newItem.Value);
audit.CommitValue = audit.CommitValue + $" {oldItem.Key} : " + oldItem.Value + $" -> " + newItem.Value + ";\n";
audit.PCommitValue = audit.PCommitValue + $" {EnumExtensions.GetDisplayProperty(oldItem.Key.Get Type())} : "
+ oldItem.Value + $" -> " + newItem.Value + "; \n";
}
}
return audit;
}
}
این کلاسها برای ثبت موارد تغییر کرده / ویرایش شده در بانک هست.
در حالت کلی هر رکوردی که ایجاد میشه خودکار ایجاد کننده و ویرایش کننده در کنار همان رکورد ثبت میشود.
این کلاس جدید برای این هست که بدانیم هر رکورد چند بار ویرایش شده و چه کسانی ویرایش را انجام داده اند. در حالت قبل فقط آخرین نفر قابل رهگیری بود ولی با این روش همه کسانی که ویرایش کرده اند را رصد میکنید.
به همین ترتیب متد AuditFields که در تاپیک قبلی هست را هم تغییر میدهیم:
private void AuditFields()
{
ChangeTracker.DetectChanges();
var userId = _accessor.HttpContext?.User?.Identity?.GetUserId() ;
var auditUser = _accessor.HttpContext?.User?.Identity?.Name;
var auditDate = DateTime.Now;
var ipAddress = HttpContextExtensionsGetRemoteIP.GetRemoteIPAddres s(_accessor.HttpContext).ToString();
var auditEntries = new List<AuditEntry>();
foreach (var entry in this.ChangeTracker.Entries<BaseEntity>())
{
var auditEntry = new AuditEntry(entry);
switch (entry.State)
{
case EntityState.Added:
entry.Entity.InsertDate = auditDate;
entry.Entity.InsertBy = auditUser;
entry.Entity.InsertedBy = userId;
entry.Entity.IsActive = true;
entry.Entity.InsertIpAddress = ipAddress;
break;
case EntityState.Modified:
entry.Entity.UpdateDate = auditDate;
entry.Entity.UpdateBy = auditUser;
entry.Entity.UpdatedBy = userId;
entry.Entity.UpdateIpAddress = ipAddress;
foreach (var property in entry.Properties)
{
auditEntry.TableName = entry.Entity.GetType().Name;
auditEntry.PersianTableName = EnumExtensions.GetClassDescription(entry.Entity.Ge tType());
auditEntries.Add(auditEntry);
string propertyName = property.Metadata.Name;
string propertyDisplayName = propertyName;
string propertyPersianDisplayName = EnumExtensions.GetDisplayProperty(property.GetType ());
if (property.Metadata.IsPrimaryKey())
{
auditEntry.KeyValues[propertyName] = property.CurrentValue;
continue;
}
if (property.IsModified)
{
auditEntry.UserId = userId;
auditEntry.UserName = auditUser;
var clx = auditEntries.Select(x => x.Entry.Entity).FirstOrDefault();
auditEntry.ChangedColumns.Add(propertyDisplayName) ;
auditEntry.PChangedColumns.Add(propertyPersianDisp layName);
auditEntry.AuditType = AuditType.Update;
auditEntry.OldValues[propertyName] = property.OriginalValue;
auditEntry.NewValues[propertyName] = property.CurrentValue;
}
}
break;
case EntityState.Deleted:
entry.Entity.DeleteDate = auditDate;
entry.Entity.DeleteBy = auditUser;
entry.Entity.UpdateIpAddress = ipAddress;
break;
}
}
if (auditEntries.Any())
{
foreach (var auditEntry in auditEntries)
{
ChangeLogs.Add(auditEntry.ToAudit());
}
}
}
متد SaveChanges که قبلا override کرده بودیم به حالت قبل باقی میماند و تغییری ندارد.
یک نکته اینجا هست جهت یادآوردی عرض میکنم. کلاس BaseEntity یک کلاس عمومی هست که همه کلاسها از اون ارث بری میکنند. همان ستونهای Audit که میبینید به این طریق ایجاد شده اند.
public class BaseEntity
{
[MaxLength(500), Display(Name = "توضیحات : "), JsonPropertyName("توضیحات")]
public string Description { get; set; }
[MaxLength(450), Display(Name = "ایجادکننده:")]
public string InsertBy { get; set; }
[MaxLength(450), Display(Name = "InsertedByUser")]
public string InsertedBy { get; set; }
[MaxLength(450), Display(Name = "UpdatedBy")]
public string UpdatedBy { get; set; }
[Display(Name = "زمان ایجاد:")]
[DefaultValue("CONVERT(DATETIME, CONVERT(VARCHAR(20),GetDate(), 120))")]
public DateTime InsertDate { get; set; }
[MaxLength(450), Display(Name = "بروز رسانی کننده:")]
public string UpdateBy { get; set; }
[Display(Name = "زمان بروزرسانی:")]
public DateTime? UpdateDate { get; set; }
[MaxLength(450), Display(Name = "حذف کننده:")]
public string DeleteBy { get; set; }
[Display(Name = "زمان حذف:")]
public DateTime? DeleteDate { get; set; }
[Display(Name = "وضعیت فعال / غیر فعال"), JsonPropertyName("IsActive")]
public bool IsActive { get; set; }
[Timestamp, JsonIgnore()]
public byte[] RowVersion { get; set; }
public string GuidClient { get; set; }
public string GuidController { get; set; }
[MaxLength(50)]
public string InsertIpAddress { get; set; }
[MaxLength(50)]
public string UpdateIpAddress { get; set; }
}
ممکنه سوال بشه که چرا UpdateBy , UpdateDate روی همه رکوردها مجدد مقدار دهی میشه! دلیلش این هست که من میخواهم هر کاربری که توی فرم داره کار میکنه هم بتونه ببینه این رکورد توسط چه کسی ایجاد شده و هم آخریش ویرایش را بتواند ببیند.
اون جدول AuditEntry که بعد از هر ویرایش ساخته میشه در واقع یک قسمت مدیریتی هست و قرار نیست که کاربران عادی بهش دسترسی داشته باشند ولی برای اینکه مراجعه به مدیریت را کم کنم این فیلدها رو هم قرار دادم.
امیدوارم که شفاف توضیح داده باشم و کمکی کرده باشم.
موفق باشید