I am using the entity framework to create an audit trail. Rather than audit every property, I thought I would create a custom attribute
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class DoNotAudit : Attribute
{
}
Then I would apply this to my model
[Table("AuditZone")]
public class AuditZone
{
public AuditZone()
{
AuditZoneUploadedCOESDetails = new List<UploadedCOESDetails>();
AuditZonePostcode = new List<Postcodes>();
}
[Key]
public int Id { get; set; }
public string Description { get; set; }
public bool Valid { get; set; }
public DateTime CreatedDate { get; set; }
public int? CreatedBy { get; set; }
[DoNotAudit]
public DateTime? ModifiedDate { get; set; }
public int? ModifiedBy { get; set; }
public virtual UserProfile CreatedByUser { get; set; }
public virtual UserProfile ModifiedByUser { get; set; }
public virtual ICollection<UploadedCOESDetails> AuditZoneUploadedCOESDetails { get; set; }
public virtual ICollection<Postcodes> AuditZonePostcode { get; set; }
}
Then in my code for the audit trail
// This is overridden to prevent someone from calling SaveChanges without specifying the user making the change
public override int SaveChanges()
{
throw new InvalidOperationException("User ID must be provided");
}
public int SaveChanges(int userId)
{
// Get all Added/Deleted/Modified entities (not Unmodified or Detached)
foreach (var ent in this.ChangeTracker.Entries().Where(p => p.State == System.Data.EntityState.Added || p.State == System.Data.EntityState.Deleted || p.State == System.Data.EntityState.Modified))
{
// For each changed record, get the audit record entries and add them
foreach (AuditLog x in GetAuditRecordsForChange(ent, userId))
{
this.AuditLogs.Add(x);
}
}
// Call the original SaveChanges(), which will save both the changes made and the audit records
return base.SaveChanges();
}
private List<AuditLog> GetAuditRecordsForChange(DbEntityEntry dbEntry, int userId)
{
List<AuditLog> result = new List<AuditLog>();
DateTime changeTime = DateTime.UtcNow;
// Get the Table() attribute, if one exists
//TableAttribute tableAttr = dbEntry.Entity.GetType().GetCustomAttributes(typeof(TableAttribute), false).SingleOrDefault() as TableAttribute;
TableAttribute tableAttr = dbEntry.Entity.GetType().GetCustomAttributes(typeof(TableAttribute), true).SingleOrDefault() as TableAttribute;
// Get table name (if it has a Table attribute, use that, otherwise get the pluralized name)
string tableName = tableAttr != null ? tableAttr.Name : dbEntry.Entity.GetType().Name;
// Get primary key value (If you have more than one key column, this will need to be adjusted)
var keyNames = dbEntry.Entity.GetType().GetProperties().Where(p => p.GetCustomAttributes(typeof(KeyAttribute), false).Count() > 0).ToList();
string keyName = keyNames[0].Name;
var test = dbEntry.Entity.GetType().GetProperties().Where(p => p.GetCustomAttributes(typeof(DoNotAudit), false).Count() > 0).ToList();
// //dbEntry.Entity.GetType().GetProperties().Single(p => p.GetCustomAttributes(typeof(KeyAttribute), false).Count() > 0).Name;
if (dbEntry.State == System.Data.EntityState.Added)
{
// For Inserts, just add the whole record
// If the entity implements IDescribableEntity, use the description from Describe(), otherwise use ToString()
foreach (string propertyName in dbEntry.CurrentValues.PropertyNames)
{
result.Add(new AuditLog()
{
AuditLogId = Guid.NewGuid(),
UserId = userId,
EventDateUTC = changeTime,
EventType = "A", // Added
TableName = tableName,
RecordId = dbEntry.CurrentValues.GetValue<object>(keyName).ToString(),
ColumnName = propertyName,
NewValue = dbEntry.CurrentValues.GetValue<object>(propertyName) == null ? null : dbEntry.CurrentValues.GetValue<object>(propertyName).ToString()
}
);
}
}
else if (dbEntry.State == System.Data.EntityState.Deleted)
{
// Same with deletes, do the whole record, and use either the description from Describe() or ToString()
result.Add(new AuditLog()
{
AuditLogId = Guid.NewGuid(),
UserId = userId,
EventDateUTC = changeTime,
EventType = "D", // Deleted
TableName = tableName,
RecordId = dbEntry.OriginalValues.GetValue<object>(keyName).ToString(),
ColumnName = "*ALL"//,
// NewValue = (dbEntry.OriginalValues.ToObject() is IDescribableEntity) ? (dbEntry.OriginalValues.ToObject() as IDescribableEntity).Describe() : dbEntry.OriginalValues.ToObject().ToString()
}
);
}
else if (dbEntry.State == System.Data.EntityState.Modified)
{
foreach (string propertyName in dbEntry.OriginalValues.PropertyNames)
{
var doNotAUditDefined = dbEntry.Property(propertyName).GetType().GetCustomAttributes(typeof(DoNotAudit), false);
// var test1 = dbEntry.Property(propertyName).GetType().Where(p => p.GetCustomAttributes(typeof(DoNotAudit), false).Count() > 0).ToList();
// var test = dbEntry.Entity.GetType().GetProperties().Where(p => p.GetCustomAttributes(typeof(DoNotAudit), false).Count() > 0).ToList();
// For updates, we only want to capture the columns that actually changed
if (!object.Equals(dbEntry.OriginalValues.GetValue<object>(propertyName), dbEntry.CurrentValues.GetValue<object>(propertyName)))
{
result.Add(new AuditLog()
{
AuditLogId = Guid.NewGuid(),
UserId = userId,
EventDateUTC = changeTime,
EventType = "M", // Modified
TableName = tableName,
RecordId = dbEntry.OriginalValues.GetValue<object>(keyName).ToString(),
ColumnName = propertyName,
OriginalValue = dbEntry.OriginalValues.GetValue<object>(propertyName) == null ? null : dbEntry.OriginalValues.GetValue<object>(propertyName).ToString(),
NewValue = dbEntry.CurrentValues.GetValue<object>(propertyName) == null ? null : dbEntry.CurrentValues.GetValue<object>(propertyName).ToString()
}
);
}
}
}
// Otherwise, don't do anything, we don't care about Unchanged or Detached entities
return result;
}
In the modified section I have the following line of code
var doNotAUditDefined = dbEntry.Property(propertyName).GetType().GetCustomAttributes(typeof(DoNotAudit), false);
WHen I step through the code, even for the modifiedDate property this is shown as empty. How can that be? any help is appreciated
Thanks
What you get with the following code:
dbEntry.Property(propertyName).GetType()
is the type of the modified property, like DateTime? in the case of ModifiedType. So there is no attribute defined on the DateTime? class. (As the attribute is defined in your AuditZone class)
What I would do is to save the list of properties that should not be audited before entering into the modified part of your audit code (at least before looping the list of modified properties). Then as looping through the modified properties, check if the property name is in the list of properties excluded from audit. Something like this:
var auditExcludedProps = dbEntry.Entity.GetType()
.GetProperties()
.Where(p => p.GetCustomAttributes(typeof(DoNotAudit), false).Any())
.Select(p => p.Name)
.ToList();
foreach (string propertyName in dbEntry.OriginalValues.PropertyNames)
{
var doNotAUditDefined = auditExcludedProps.Contains(propertyName);
...
}
You may want to double check that dbEntry.Entity.GetType() returns your class AuditZone and the list auditExcludedProps contains the ModifiedDate property.
Hope it helps!
Related
I have the following model that has these fields:
[Key]
public string Id { get; set; }
[IsSearchable]
public string Code{ get; set; }
[IsSearchable]
public string Name { get; set; }
[IsSearchable]
public string Address { get; set; }
[IsSearchable]
public string PostCode { get; set; }
[IsFilterable]
public int? Setting{ get; set; }
[IsFilterable, IsSortable]
public Location Location { get; set; }
I am writing a method to compare whether values in a database match this model. So far it looks like this:
private bool CompareEquality(Index resultBody, Type indexType)
{
var properties = indexType.GetProperties(BindingFlags.Instance | BindingFlags.Public);
List<PropertyInfo> searchableProperties = new List<PropertyInfo>();
List<PropertyInfo> filterableProperties = new List<PropertyInfo>();
List<PropertyInfo> sortableProperties = new List<PropertyInfo>();
if (properties.Count() == resultBody.Fields.Count)
{
foreach (var property in properties)
{
var isSearchableAttribute = property.GetCustomAttribute<IsSearchableAttribute>();
var isFilterableAttribute = property.GetCustomAttribute<IsFilterableAttribute>();
var isSortableAttribute = property.GetCustomAttribute<IsSortableAttribute>();
if (isSearchableAttribute != null)
{
searchableProperties.Add(property);
}
if (isFilterableAttribute != null)
{
filterableProperties.Add(property);
}
if (isSortableAttribute != null)
{
sortableProperties.Add(property);
}
}
CheckAttributeEquality(searchableProperties, filterableProperties, sortableProperties);
}
return false;
}
The CheckAttributeEquality method:
private bool CheckAttributeEquality(List<PropertyInfo> searchableProperties, List<PropertyInfo> filterableProperties, List<PropertyInfo> sortableProperties)
{
if (searchableProperties.Count == 4 && filterableProperties.Count == 2 && sortableProperties.Count == 1)
{
CheckPropertyFields(searchableProperties, filterableProperties, sortableProperties);
return true;
}
return false;
}
As I started to write a method to check that the field names match, like so:
foreach (var property in searchableProperties)
{
if (property.Name == "Id" ||)
{
...
}
if (property.Name == "Code")
{
...
}
// etc
I realised how messy and long-winded this whole approach is. I am not hugely experienced in C# and would appreciate any advice as to how I can refactor this up a little bit? I want to check for attribute and name matches.
you could use the Typedescriptor (using System.ComponentModel) for that. Try this:
var pdc = TypeDescriptor.GetProperties( this ); //or your model object, if its not "this"
foreach (var property in searchableProperties)
{
var descriptors = pdc[ property.Name ];
// check if your searchable descriptor is there, and do error handling
}
Once it works, you could also try to solve it with LINQ.
I have the following classes,
User:
public class User:Domain
{
[Sortable]
public string FirstName { get; set; }
[Sortable]
public string LastName { get; set; }
[NestedSortable]
public Profile Profile { get; set; }
}
Profile:
public class Profile : Domain
{
[Sortable]
public string BrandName { get; set; }
[NestedSortable]
public Client Client { get; set; }
[NestedSortable]
public ProfileContact ProfileContact { get; set; }
}
Client:
public class Client : Domain
{
[Sortable]
public string Name { get; set; }
}
Profile Contact:
public class ProfileContact : Domain
{
[Sortable]
public string Email { get; set; }
}
I'm writing a recursive function to get all the properties decorated with [Sortable] attribute. This works well when I have a single [NestedSortableProperty] but fails when I have more than one.
Here is my recursive function:
private static IEnumerable<SortTerm> GetTermsFromModel(
Type parentSortClass,
List<SortTerm> sortTerms,
string parentsName = null,
bool hasNavigation = false)
{
if (sortTerms is null)
{
sortTerms = new List<SortTerm>();
}
sortTerms.AddRange(parentSortClass.GetTypeInfo()
.DeclaredProperties
.Where(p => p.GetCustomAttributes<SortableAttribute>().Any())
.Select(p => new SortTerm
{
ParentName = parentSortClass.Name,
Name = hasNavigation ? $"{parentsName}.{p.Name}" : p.Name,
EntityName = p.GetCustomAttribute<SortableAttribute>().EntityProperty,
Default = p.GetCustomAttribute<SortableAttribute>().Default,
HasNavigation = hasNavigation
}));
var complexSortProperties = parentSortClass.GetTypeInfo()
.DeclaredProperties
.Where(p => p.GetCustomAttributes<NestedSortableAttribute>().Any());
if (complexSortProperties.Any())
{
foreach (var parentProperty in complexSortProperties)
{
var parentType = parentProperty.PropertyType;
if (string.IsNullOrWhiteSpace(parentsName))
{
parentsName = parentType.Name;
}
else
{
parentsName += $".{parentType.Name}";
}
return GetTermsFromModel(parentType, sortTerms, parentsName, true);
}
}
return sortTerms;
}
this happens because of the return statement inside the foreach loop. How to rewrite this? with this example I need to get a list of FirstName,LastName,BrandName,Name and Email. But I'm getting only the first four properties except Email.
Now that the above issue is resolved by removing the return statement as posted in my own answer below and also following #Dialecticus comments to use yield return. so I strike and updated the question.
Now I'm running into another issue. The parent class name is wrongly assigned if a class has multiple [NestedSortable] properties.
This method is called first time with User class like var declaredTerms = GetTermsFromModel(typeof(User), null);
Example,
After the first call, the parentsName parameter will be null and [Sortable] properties in User class don't have any effect.
Now for the [NestedSortable] Profile property in User class, the parentsName will be Profile and so the [Sortable] properties in Profile class will have Name as Profile.BrandName and so on.
Name property in final list to be as follows,
Expected Output:
FirstName, LastName, Profile.BrandName, Profile.Client.Name, Profile.ProfileContact.Email
But Actual Output:
FirstName, LastName, Profile.BrandName, Profile.Client.Name, Profile.Client.ProfileContact.Email
Please assist on how to fix this.
Thanks,
Abdul
after some debugging, I removed the return statement inside foreach loop and that fixed the first issue.
changed from,
return GetTermsFromModel(parentType, sortTerms, parentsName, true);
to,
GetTermsFromModel(parentType, sortTerms, parentsName, true);
Then as per #Dialecticus comments removed passing sortTerms as input parameter and removed the parameter inside the code and changed sortTerms.AddRange(...) to yield return.
changed from,
sortTerms.AddRange(parentSortClass.GetTypeInfo()
.DeclaredProperties
.Where(p => p.GetCustomAttributes<SortableAttribute>().Any())
.Select(p => new SortTerm
{
ParentName = parentSortClass.Name,
Name = hasNavigation ? $"{parentsName}.{p.Name}" : p.Name,
EntityName = p.GetCustomAttribute<SortableAttribute>().EntityProperty,
Default = p.GetCustomAttribute<SortableAttribute>().Default,
HasNavigation = hasNavigation
}));
to,
foreach (var p in properties)
{
yield return new SortTerm
{
ParentName = parentSortClass.Name,
Name = hasNavigation ? $"{parentsName}.{p.Name}" : p.Name,
EntityName = p.GetCustomAttribute<SortableAttribute>().EntityProperty,
Default = p.GetCustomAttribute<SortableAttribute>().Default,
HasNavigation = hasNavigation
};
}
also for complex properties, changed from,
GetTermsFromModel(parentType, sortTerms, parentsName, true);
to,
var complexProperties = GetTermsFromModel(parentType, parentsName, true);
foreach (var complexProperty in complexProperties)
{
yield return complexProperty;
}
And for the final issue I'm facing with the name, adding the below code after the inner foreach loop fixed it,
parentsName = parentsName.Replace($".{parentType.Name}", string.Empty);
So here is the complete updated working code:
private static IEnumerable<SortTerm> GetTermsFromModel(
Type parentSortClass,
string parentsName = null,
bool hasNavigation = false)
{
var properties = parentSortClass.GetTypeInfo()
.DeclaredProperties
.Where(p => p.GetCustomAttributes<SortableAttribute>().Any());
foreach (var p in properties)
{
yield return new SortTerm
{
ParentName = parentSortClass.Name,
Name = hasNavigation ? $"{parentsName}.{p.Name}" : p.Name,
EntityName = p.GetCustomAttribute<SortableAttribute>().EntityProperty,
Default = p.GetCustomAttribute<SortableAttribute>().Default,
HasNavigation = hasNavigation
};
}
var complexSortProperties = parentSortClass.GetTypeInfo()
.DeclaredProperties
.Where(p => p.GetCustomAttributes<NestedSortableAttribute>().Any());
if (complexSortProperties.Any())
{
foreach (var parentProperty in complexSortProperties)
{
var parentType = parentProperty.PropertyType;
//if (string.IsNullOrWhiteSpace(parentsName))
//{
// parentsName = parentType.Name;
//}
//else
//{
// parentsName += $".{parentType.Name}";
//}
var complexProperties = GetTermsFromModel(parentType, string.IsNullOrWhiteSpace(parentsName) ? parentType.Name : $"{parentsName}.{parentType.Name}", true);
foreach (var complexProperty in complexProperties)
{
yield return complexProperty;
}
//parentsName = parentsName.Replace($".{parentType.Name}", string.Empty);
}
}
}
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));
}
I am inserting 5000 transactions with about 6 banks, but I get 5000 bank rows in the database with duplicate names.
public class Transaction
{
[Key]
public int Id { get; set; }
public DateTime TransDate { get; set; }
public decimal Value { get; set; }
public string Description { get; set; }
public Bank Bank { get; set; }
}
public class Bank
{
[Key]
public int Id { get; set; }
public string Name { get; set; }
}
public class FinancialRecordContext : DbContext
{
public FinancialRecordContext() : base("FinancialRecordDatabase") { }
public DbSet<Transaction> Transactions { get; set; }
public DbSet<Bank> Banks { get; set; }
public Bank FindOrInsertBank(string bankName)
{
var bank = Banks.SingleOrDefault(b => b.Name == bankName);
if (bank == null)
{
bank = new Bank { Name = bankName };
Banks.Add(bank);
}
return bank;
}
}
Then to insert I am looping through some data and inserting thusly:
using (var context = new FinancialRecordContext())
{
foreach (var t in data)
{
var tran = new Transaction
{
Description = t.Description,
Value = t.Value,
TransDate = t.TransDate,
Bank = context.FindOrInsertBank(t.BankName)
};
context.Transactions.Add(tran);
}
context.SaveChanges();
}
It would appear that the FindOrInsertBank method is going to the database all the time and not looking locally at it's recently added, but not committed banks. How can/should I be doing this?
Should I SaveChanges after each bank insert? Not really what I want to do I want this to be all one transaction.
A couple of suggestions to try:
1) Check the Local collection of DbSet<Bank> first.
public Bank FindOrInsertBank(string bankName)
{
var bank = Banks.Local.SingleOrDefault(b => b.Name == bankName);
if (bank == null)
{
var bank = Banks.SingleOrDefault(b => b.Name == bankName);
if (bank == null)
{
bank = new Bank { Name = bankName };
Banks.Add(bank);
}
}
return bank;
}
2) Force a call to DetectChanges() after each update
context.Transactions.Add(tran);
context.ChangeTracker.DetectChanges();
You can try to query the change tracker (which is an in-memory query, not a database query):
using (var context = new FinancialRecordContext())
{
foreach (var t in data)
{
Bank bank = context.ChangeTracker.Entries()
.Where(e => e.Entity is Bank && e.State != EntityState.Deleted)
.Select(e => e.Entity as Bank)
.SingleOrDefault(b => b.Name == t.BankName);
if (bank == null)
bank = context.FindOrInsertBank(t.BankName);
var tran = new Transaction
{
Description = t.Description,
Value = t.Value,
TransDate = t.TransDate,
Bank = bank
};
context.Transactions.Add(tran);
}
context.SaveChanges();
}
Edit
Using the change tracker here could be bad for performance because Bank.Name is not the key of the entity and I guess the query would be a linear search through the entries. In this case using a handwritten dictionary might be the better solution:
using (var context = new FinancialRecordContext())
{
var dict = new Dictionary<string, Bank>();
foreach (var t in data)
{
Bank bank;
if (!dict.TryGetValue(t.BankName, out bank))
{
bank = context.FindOrInsertBank(t.BankName);
dict.Add(t.BankName, bank);
}
var tran = new Transaction
{
Description = t.Description,
Value = t.Value,
TransDate = t.TransDate,
Bank = bank
};
context.Transactions.Add(tran);
}
context.SaveChanges();
}
I have a class called Section
public class Section
{
public Section() { construct(0); }
public Section(int order) { construct(order); }
private void construct(int order)
{
Children = new List<Section>();
Fields = new List<XfaField>();
Hint = new Hint();
Order = order;
}
[Key]
public int Id { get; set; }
public int FormId { get; set; }
public string Name { get; set; }
[InverseProperty("Parent")]
public List<Section> Children { get; set; }
public List<XfaField> Fields { get; set; }
public Section Parent { get; set; }
public Hint Hint { get; set; }
public int Order { get; private set; }
#region Methods
public void AddNewChild()
{
AddChild(new Section
{
Name = "New Child Section",
FormId = FormId,
});
}
private void AddChild(Section child)
{
child.Parent = this;
if (Children == null) Children = new List<Section>();
int maxOrder = -1;
if(Children.Count() > 0) maxOrder = Children.Max(x => x.Order);
child.Order = ++maxOrder;
Children.Add(child);
FactoryTools.Factory.PdfSections.Add(child);
}
// Other methods here
#endregion
}
I am trying to add a new child Section to an already existing parent like this:
private void AddChildSection()
{
var parent = FactoryTools.Factory.PdfSections.FirstOrDefault(x => x.Id == ParentId);
if (parent == null) throw new Exception("Unable to create child because parent with Id " + ParentId.ToString() + " doesn't exist.");
parent.AddNewChild();
FactoryTools.Factory.SaveChanges();
}
When I look at the database, I see that a new row has been added, so for example:
Id Name Parent_Id Hint_Id FormId Order
19 New Child Section 1 27 1 0
However, when I load the parent Section, the Children property is always of Count 0, like this:
public ActionResult EditSection(int formId, int sectionId)
{
var model = FactoryTools.Factory.PdfSections.FirstOrDefault(x => x.Id == sectionId);
if (model == null || model.FormId != formId) model = new Section();
//model.Children = FactoryTools.Factory.PdfSections.Where(x => x.Parent.Id == sectionId).ToList();
return PartialView(model);
}
Of course, when I manually add the children, then they are there (in the above code, by uncommenting the model.Children = ... line)
I am used to the NHibernate way of doing things and am therefore quite frustrated that the above, seemingly simple, task is not working in EntityFramework, what am I doing wrong?
Entity Framework won't eagerly load related entities. Try forcing it to include the children:
var model = FactoryTools.Factory.PdfSections.Include("Children").FirstOrDefault(x => x.Id == sectionId);
There's also a strongly-typed overload to which you can pass a lambda:
var model = FactoryTools.Factory.PdfSections.Include(s => s.Children).FirstOrDefault(x => x.Id == sectionId);