This question already has answers here:
how to create an audit trail with Entity framework 5 and MVC 4
(8 answers)
Closed 2 years ago.
I want to create a History/Audit Table for a particular entity. This is a complex entity with many child tables and we are using Repository Patter for our application.
I looked into overriding DbContext SaveChanges?. Is it good practice to use this specially for one entity?.
What are my other options?.
Thanks in advance.
I've been working on a library that might help.
Take a look at Audit.EntityFramework library, it intercepts SaveChanges() and can be configured to filter the entities you want to audit.
#thepirat000 solution probably works fine but I l like to have a minimum of NuGet dependencies, preferably 0, that are not backed by a large community/corporation and that depends heavily on a single developer.
https://github.com/thepirat000/Audit.NET/graphs/contributors
You can do it like this without any external library:
using (var context = new SampleContext())
{
// Insert a row
var customer = new Customer();
customer.FirstName = "John";
customer.LastName = "doe";
context.Customers.Add(customer);
await context.SaveChangesAsync();
// Update the first customer
customer.LastName = "Doe";
await context.SaveChangesAsync();
// Delete the customer
context.Customers.Remove(customer);
await context.SaveChangesAsync();
}
Model:
public class Audit
{
public int Id { get; set; }
public string TableName { get; set; }
public DateTime DateTime { get; set; }
public string KeyValues { get; set; }
public string OldValues { get; set; }
public string NewValues { get; set; }
}
public class Customer
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
}
public class SampleContext : DbContext
{
public DbSet<Customer> Customers { get; set; }
public DbSet<Audit> Audits { get; set; }
}
DbContext:
public class SampleContext : DbContext
{
public DbSet<Customer> Customers { get; set; }
public DbSet<Audit> Audits { get; set; }
public override async Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default(CancellationToken))
{
var auditEntries = OnBeforeSaveChanges();
var result = await base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
await OnAfterSaveChanges(auditEntries);
return result;
}
private List<AuditEntry> OnBeforeSaveChanges()
{
ChangeTracker.DetectChanges();
var auditEntries = new List<AuditEntry>();
foreach (var entry in ChangeTracker.Entries())
{
if (entry.Entity is Audit || entry.State == EntityState.Detached || entry.State == EntityState.Unchanged)
continue;
var auditEntry = new AuditEntry(entry);
auditEntry.TableName = entry.Metadata.Relational().TableName;
auditEntries.Add(auditEntry);
foreach (var property in entry.Properties)
{
if (property.IsTemporary)
{
// value will be generated by the database, get the value after saving
auditEntry.TemporaryProperties.Add(property);
continue;
}
string propertyName = property.Metadata.Name;
if (property.Metadata.IsPrimaryKey())
{
auditEntry.KeyValues[propertyName] = property.CurrentValue;
continue;
}
switch (entry.State)
{
case EntityState.Added:
auditEntry.NewValues[propertyName] = property.CurrentValue;
break;
case EntityState.Deleted:
auditEntry.OldValues[propertyName] = property.OriginalValue;
break;
case EntityState.Modified:
if (property.IsModified)
{
auditEntry.OldValues[propertyName] = property.OriginalValue;
auditEntry.NewValues[propertyName] = property.CurrentValue;
}
break;
}
}
}
// Save audit entities that have all the modifications
foreach (var auditEntry in auditEntries.Where(_ => !_.HasTemporaryProperties))
{
Audits.Add(auditEntry.ToAudit());
}
// keep a list of entries where the value of some properties are unknown at this step
return auditEntries.Where(_ => _.HasTemporaryProperties).ToList();
}
private Task OnAfterSaveChanges(List<AuditEntry> auditEntries)
{
if (auditEntries == null || auditEntries.Count == 0)
return Task.CompletedTask
foreach (var auditEntry in auditEntries)
{
// Get the final value of the temporary properties
foreach (var prop in auditEntry.TemporaryProperties)
{
if (prop.Metadata.IsPrimaryKey())
{
auditEntry.KeyValues[prop.Metadata.Name] = prop.CurrentValue;
}
else
{
auditEntry.NewValues[prop.Metadata.Name] = prop.CurrentValue;
}
}
// Save the Audit entry
Audits.Add(auditEntry.ToAudit());
}
return SaveChangesAsync();
}
}
public class AuditEntry
{
public AuditEntry(EntityEntry entry)
{
Entry = entry;
}
public EntityEntry Entry { get; }
public string TableName { 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 List<PropertyEntry> TemporaryProperties { get; } = new List<PropertyEntry>();
public bool HasTemporaryProperties => TemporaryProperties.Any();
public Audit ToAudit()
{
var audit = new Audit();
audit.TableName = TableName;
audit.DateTime = DateTime.UtcNow;
audit.KeyValues = JsonConvert.SerializeObject(KeyValues);
audit.OldValues = OldValues.Count == 0 ? null : JsonConvert.SerializeObject(OldValues);
audit.NewValues = NewValues.Count == 0 ? null : JsonConvert.SerializeObject(NewValues);
return audit;
}
}
Source:
https://www.meziantou.net/entity-framework-core-history-audit-table.htm and comment from #rasputino
You can also read more about Slowly changing dimension types and from there create a solution that fits your needs.
If you need entire Entity Framework Snapshot History look at this answer.
Related
I'm trying to insert some test values into an Azure table using storage connection string. When I tried to perform insert operation it showing error as cannot convert Entities.SyncJob to Microsoft.Azure.CosmosDB.Table.ITableEntity.
public async Task BackupJobsAsync(List<SyncJob> syncJobs)
{
var tableName = "Table";
var table = await GetCloudTableAsync("connectionString", tableName);
if (!await table.ExistsAsync())
{
await table.CreateIfNotExistsAsync();
}
var backupCount = 0;
var batchSize = 100;
var currentSize = 0;
var groups = syncJobs.GroupBy(x => x.PartitionKey).ToList();
foreach (var group in groups)
{
var batchOperation = new TableBatchOperation();
foreach (var job in group)
{
batchOperation.Insert(job);
if (++currentSize == batchSize)
{
var result = await table.ExecuteBatchAsync(batchOperation);
backupCount += result.Count(x => IsSuccessStatusCode(x.HttpStatusCode));
batchOperation = new TableBatchOperation();
currentSize = 0;
}
}
if (batchOperation.Any())
{
var result = await table.ExecuteBatchAsync(batchOperation);
backupCount += result.Count(x => IsSuccessStatusCode(x.HttpStatusCode));
}
}
}
SyncJob class:
namespace Entities
{
public class SyncJob : ITableEntity
{
public SyncJob()
{
}
public SyncJob(string partitionKey, string rowKey)
{
PartitionKey = partitionKey;
RowKey = rowKey;
}
public string PartitionKey { get; set; }
public string RowKey { get; set; }
public string ABC { get; set; }
public string DEF { get; set; }
public string GHI { get; set; }
public string JKL { get; set; }
}
}
Error Screenshot for reference. Can anyone let me know how can we overcome this? Have tried by casting the value, but the result is same.
UPDATE:
I just noticed that this is using a deprecated package nuget package Microsoft.Azure.Cosmos.Table. How do I update this code to use new package Azure.Data.Tables?
hello community I am implementing a system to audit the modifications that are made in my application carry out the process that is described in this article:
https://codewithmukesh.com/blog/audit-trail-implementation-in-aspnet-core/?unapproved=50671&moderation-hash=71700d12d4ebaf51ad9d90c4a9834324#comment-50671
but I don't know how to get the login of my application, to login I use web token and authentication provider.
any suggestion how to do it? I don't know if it can be done with serilog or something similar
this is my code:
public class Audit
{
public int Id { get; set; }
public string UserId { get; set; }
public string Type { get; set; }
public string TableName { get; set; }
public DateTime DateTime { get; set; }
public string OldValues { get; set; }
public string NewValues { get; set; }
public string AffectedColumns { get; set; }
public string PrimaryKey { get; set; }
}
public enum AuditType
{
None = 0,
Create = 1,
Update = 2,
Delete = 3
}
public class AuditEntry
{
public AuditEntry(EntityEntry entry)
{
Entry = entry;
}
public EntityEntry Entry { get; }
public string UserId { get; set; }
public string TableName { 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 Audit ToAudit()
{
var audit = new Audit();
audit.UserId = UserId;
audit.Type = AuditType.ToString();
audit.TableName = TableName;
audit.DateTime = DateTime.Now;
audit.PrimaryKey = JsonConvert.SerializeObject(KeyValues);
audit.OldValues = OldValues.Count == 0 ? null : JsonConvert.SerializeObject(OldValues);
audit.NewValues = NewValues.Count == 0 ? null : JsonConvert.SerializeObject(NewValues);
audit.AffectedColumns = ChangedColumns.Count == 0 ? null : JsonConvert.SerializeObject(ChangedColumns);
return audit;
}
}
public abstract class AuditableIdentityContext : IdentityDbContext
{
public AuditableIdentityContext(DbContextOptions options) : base(options)
{
}
public DbSet<Audit> AuditLogs { get; set; }
public virtual async Task<int> SaveChangesAsync(string userId = null)
{
OnBeforeSaveChanges(userId);
var result = await base.SaveChangesAsync();
return result;
}
private void OnBeforeSaveChanges(string userId)
{
ChangeTracker.DetectChanges();
var auditEntries = new List<AuditEntry>();
foreach (var entry in ChangeTracker.Entries())
{
if (entry.Entity is Audit || entry.State == EntityState.Detached || entry.State == EntityState.Unchanged)
continue;
var auditEntry = new AuditEntry(entry);
auditEntry.TableName = entry.Entity.GetType().Name;
auditEntry.UserId = userId;
auditEntries.Add(auditEntry);
foreach (var property in entry.Properties)
{
string propertyName = property.Metadata.Name;
if (property.Metadata.IsPrimaryKey())
{
auditEntry.KeyValues[propertyName] = property.CurrentValue;
continue;
}
switch (entry.State)
{
case EntityState.Added:
auditEntry.AuditType = Enums.AuditType.Create;
auditEntry.NewValues[propertyName] = property.CurrentValue;
break;
case EntityState.Deleted:
auditEntry.AuditType = Enums.AuditType.Delete;
auditEntry.OldValues[propertyName] = property.OriginalValue;
break;
case EntityState.Modified:
if (property.IsModified)
{
auditEntry.ChangedColumns.Add(propertyName);
auditEntry.AuditType = Enums.AuditType.Update;
auditEntry.OldValues[propertyName] = property.OriginalValue;
auditEntry.NewValues[propertyName] = property.CurrentValue;
}
break;
}
}
}
foreach (var auditEntry in auditEntries)
{
AuditLogs.Add(auditEntry.ToAudit());
}
}
}
I use a custom Controller that inherits from the built-in Controller, and added a CurrentUserName property. The problem is that Controller.User is not yet initialized in the Controller constructor. I override Controller.OnActionExecuting(), where the User exists, and use that to set my custom property.
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var authenticatedUser = User;
if (authenticatedUser != null)
{
string userId = authenticatedUser.FindFirstValue(ClaimTypes.NameIdentifier);
if (userId != null)
{
_currentUserId = int.Parse(userId, CultureInfo.InvariantCulture);
string userName = authenticatedUser.FindFirstValue(ClaimTypes.Name);
_context.CurrentUserName = userName;
_userIsAdmin = authenticatedUser.IsInRole(ConferenceRoleTypes.WebAdmin);
}
}
base.OnActionExecuting(filterContext);
}
I want to display a DialogForm in my RootDialog. I've tried to do it by doing like calling forms from dialogs. However, my problem here is I have a model class generated by Entity Framework which contains some fields about primary key (or Foreign key) that I don't want my client to enter input for it. So my question is how can I make a DialogForm that just ask my clients to enter the fields that I want to?
Here's my Model class:
[Serializable]
public partial class BOT_CUSTOMERINFO
{
public int CUSTOMER_ID { get; set; }
public Nullable<int> DOMAIN_ID { get; set; }
public string NAME { get; set; }
public string EMAIL { get; set; }
public string PHONE { get; set; }
public Nullable<int> RECORD_STATUS { get; set; }
public static IForm<BOT_CUSTOMERINFO> BuildForm()
{
return new FormBuilder<BOT_CUSTOMERINFO>()
.Message("Vui lòng cung cấp thông tin")
.Field(nameof(NAME))
.Field(nameof(EMAIL))
.Field(nameof(PHONE))
.Field(new FieldReflector<BOT_CUSTOMERINFO>(nameof(CUSTOMER_ID)).SetActive((state) => false))
.Build();
}
}
And hence what I've used to call FormDialog:
private BuildFormDelegate<Models.FormDialog_Models.CustomerInfoFormModel> MakeCustomerInfoForm;
internal void CustomerInfoDialog(BuildFormDelegate<Models.BOT_CUSTOMERINFO> makeCustomerInfoForm)
{
this.MakeCustomerInfoForm = makeCustomerInfoForm;
}
public async Task ResumeAfterChooseQuestion(IDialogContext context, IAwaitable<BOT_QUESTION> result)
{
var value = await result;
if(value != null)
{
BotDBEntities DbContext = new BotDBEntities();
if(DbContext.BOT_ANSWER.Any(answer => answer.QUESTION_ID == value.QUESTION_ID))
{
List<BOT_ANSWER> ListAnswer = DbContext.BOT_ANSWER.Where(answer => answer.QUESTION_ID == value.QUESTION_ID).ToList();
await ShowListAnswer(context, ListAnswer);
//PromptDialog.Choice(context, this.ResumeAfterChooseAnswer,ListAnswer, "Click để chọn:", "Không hợp lệ", 3, PromptStyle.Auto);
}
if(DbContext.BOT_QUESTION.Any(question => question.PREVQUESTION_ID == value.QUESTION_ID))
{
List<BOT_QUESTION> ListQuestion = DbContext.BOT_QUESTION.Where(question => question.PREVQUESTION_ID == value.QUESTION_ID).ToList();
await this.ShowListQuestion(context, ListQuestion);
}
if(value.QUESTION_TYPE.Value == 1)
{
var customerInfoForm = new FormDialog<Models.BOT_CUSTOMERINFO>(new Models.BOT_CUSTOMERINFO(), MakeCustomerInfoForm, FormOptions.PromptInStart);
context.Call(customerInfoForm, CustomerInfoFormCompleted);
// var customerInfoForm = new FormDialog<Models.FormDialog_Models.CustomerInfoFormModel>(new Models.FormDialog_Models.CustomerInfoFormModel(),MakeCustomerInfoForm, FormOptions.PromptInStart);
// context.Forward(customerInfoForm, CustomerInfoFormCompleted);
// context.Call(customerInfoForm, CustomerInfoFormCompleted);
//context.Call<Idia<BOT_CUSTOMERINFO>>(BOT_CUSTOMERINFO.BuildForm(), CustomerInfoFormCompleted);
}
}
else
{
context.Wait(this.MessageReceiveAsync);
}
}
In the declaration of your FormBuilder<BOT_CUSTOMERINFO> you can deactivate those fields, using the Advanced.Field.SetActive.
new FormBuilder<BOT_CUSTOMERINFO>
.Field(new FieldReflector<BOT_CUSTOMERINFO>(nameof(CUSTOMER_ID))
.SetActive((state) => false);
I'm trying to create an object and insert to the database but keep getting the same error no matter what I try.
The row that I get the error on is ColumnGroupTest.ValidValues.Add(memberComment1); the error is
error message
NullReferenceException was unhandled by user code
my models
public class StoreColumnName
{
public int Id { get; set; }
public string StoreColumnGroupName { get; set; }
public string ColumnName { get; set; }
public string ColumnType { get; set; }
public List<StoreValidValue> ValidValues { get; set; }
}
public class StoreValidValue
{
public int Id { get; set; }
public string ValidValue { get; set; }
public StoreColumnName StoreColumnName { get; set; }
}
my controller
public ActionResult Index()
{
XDocument document = XDocument.Load(#"C:\Users\Physical.xml");
var result = document.Descendants("ColumnGroup");
foreach(var item in result){
var ColumnGroupName = item.Attribute("name").Value;
var Columns = item.Descendants("Column");
foreach (var itemColumn in Columns)
{
StoreColumnName ColumnGroup = new StoreColumnName();
var ColumnGroupTest = new StoreColumnName
{
StoreColumnGroupName = ColumnGroupName,
ColumnName = itemColumn.Attribute("name").Value,
ColumnType = itemColumn.Attribute("type").Value,
Id = 11
};
var ValidValues = itemColumn.Descendants("ValidValues");
var Values = ValidValues.Descendants("Value");
foreach (var Value in Values)
{
var memberComment1 = new StoreValidValue
{
StoreColumnName = ColumnGroupTest,
ValidValue = Value.Value,
Id = 101
};
ColumnGroupTest.ValidValues.Add(memberComment1);
}
}
}
return View();
}
(I gladly take tips on what I can improve when asking for help/guiding here).
Can anyone help ?
The issue that you're having is that you don't initialize your ValidValues property to a list. By default, those types of properties initialize to null unless you specify differently.
The best approach is to add that initialization to your constructor of that object.
public StoreColumnName() {
this.ValidValues = new List<StoreValidValue>();
}
In my Database, I have a key/value table, which I use to store the configuration of the application. Some example settings of the StoreConfiguration class, that I want to map to this key/value table.
public class StoreConfiguration
{
//.... more settings
public int DefaultPartnerID { get; set; }
public int DefaultOperatorID { get; set; }
public string DefaultCurrency { get; set; }
public int DefaultCurrencyID { get; set; }
//.... more settings
I would like to have those in my DataBase as this for example
Key | Value
--------------------------
DefaultpartnerID | 1
DefaultOperatorID | 10
DefaultCurrency | USD
DefaultCurrencyID | 2
Is it possible to create such type of mapping with EntityFramework ?
You might want to use a simple entity that contains Key and Value property.
public class StoreConfiguration
{
[Key]
public string Key { get; set; }
public string Value { get; set; }
}
Then provide an extension to add and remove the store configuration.
public static class StoreConfigurationExtension
{
public static T GetStoreConfiguration<T>(this DbContext db, string key)
{
var sc = db.Set<StoreConfiguration>().Find(key);
if (sc == null) return default(T);
var value = sc.Value;
var tc = TypeDescriptor.GetConverter(typeof(T));
try
{
var convertedValue = (T)tc.ConvertFromString(value);
return convertedValue;
}
catch (NotSupportedException)
{
return default(T);
}
}
public static void SetStoreConfiguration(this DbContext db, string key, object value)
{
var sc = db.Set<StoreConfiguration>().Find(key);
if (sc == null)
{
sc = new StoreConfiguration { Key = key };
db.Set<StoreConfiguration>().Add(sc);
}
sc.Value = value == null ? null : value.ToString();
}
}
Usage.
using (var db = new AppContext())
{
db.SetStoreConfiguration("DefaultpartnerID", 1);
db.SaveChanges();
}
using (var db = new AppContext())
{
var defaultpartnerID = db.GetStoreConfiguration<int>("DefaultpartnerID");
db.SaveChanges();
}
No, entity framework is not meant for this. But you still can use reflection to load data into this class.
var entities = ...;
var o = new StoreConfiguration();
foreach(var p in typeof(StoreConfiguration).GetProperties())
{
var entity = entities.FirstOrDefault(e=>e.Key == p.Name);
if (entity == null) continue;
var converter = TypeDescriptor.GetConvertor(p.Type);
p.SetValue(o, converter.ConvertFromString(entity.Value));
}