I have looked for alot of different approaches to achive this. But I have yet to find a good and simple exmaple on how to do this without alot of 3 party instalations wich focuses on performance logging and debugging.
I am looking for a way to easily log ALL changes to my database and also when new rows are added. I want to have a own table that stores which action in which controller was called / or simply the database table would be enough to track it. And the which fields where updated or added.
I am picturing a table something like this:
ID - ACTION/TABLE/METHOD - ID - TYPE - DETAILS - CREATED BY - TIMESTAMP
x - TableName/ActionResult/JsonResult/ - ID of the new or updated item - updated or new - details on what have changed or created - user.identity - timestamp
So i can view the log table in each spesific view and i can see the history for that item and which fields where changed etc.
I looked at the bottom suggestion here: How to implement a MVC 4 change log? since my SQL database does not support the SQL Service Broker and I dont really want to start with adding Triggers in SQL.
I am using MVC 5.2 and EF 6.0 so I have looked at the Database.Log property but I really need some guidance on how to set up a good method to achive what I want.
I found a solution i am currently modifying to my needs.
Here is the code:
Overriding SaveChanges Class in
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
and adding theese methods:
public async Task SaveChangesAsync(string userId)
{
// Get all Added/Deleted/Modified entities (not Unmodified or Detached)
foreach (var ent in this.ChangeTracker.Entries().Where(p => p.State == EntityState.Added || p.State == EntityState.Deleted || p.State == EntityState.Modified))
{
// For each changed record, get the audit record entries and add them
foreach (Log x in GetAuditRecordsForChange(ent, userId))
{
this.Log.Add(x);
}
}
// Call the original SaveChanges(), which will save both the changes made and the audit records
await base.SaveChangesAsync();
}
private List<Log> GetAuditRecordsForChange(DbEntityEntry dbEntry, string userId)
{
List<Log> result = new List<Log>();
DateTime changeTime = DateTime.Now;
// Get the Table() attribute, if one exists
TableAttribute tableAttr = dbEntry.Entity.GetType().GetCustomAttributes(typeof(TableAttribute), false).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)
string keyName = dbEntry.Entity.GetType().GetProperties().Single(p => p.GetCustomAttributes(typeof(KeyAttribute), false).Count() > 0).Name;
if (dbEntry.State == EntityState.Added)
{
// For Inserts, just add the whole record
// If the entity implements IDescribableEntity, use the description from Describe(), otherwise use ToString()
result.Add(new Log()
{
LogID = Guid.NewGuid(),
EventType = "A", // Added
TableName = tableName,
RecordID = dbEntry.CurrentValues.GetValue<object>(keyName).ToString(), // Again, adjust this if you have a multi-column key
ColumnName = "*ALL", // Or make it nullable, whatever you want
NewValue = (dbEntry.CurrentValues.ToObject() is IDescribableEntity) ? (dbEntry.CurrentValues.ToObject() as IDescribableEntity).Describe() : dbEntry.CurrentValues.ToObject().ToString(),
Created_by = userId,
Created_date = changeTime
}
);
}
else if (dbEntry.State == EntityState.Deleted)
{
// Same with deletes, do the whole record, and use either the description from Describe() or ToString()
result.Add(new Log()
{
LogID = Guid.NewGuid(),
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(),
Created_by = userId,
Created_date = changeTime
}
);
}
else if (dbEntry.State == EntityState.Modified)
{
foreach (string propertyName in dbEntry.OriginalValues.PropertyNames)
{
// 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 Log()
{
LogID = Guid.NewGuid(),
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(),
Created_by = userId,
Created_date = changeTime
}
);
}
}
}
// Otherwise, don't do anything, we don't care about Unchanged or Detached entities
return result;
}
public DbSet<Log> Log { get; set; }
And here is the Log class
[Table("N_Log")]
public class Log
{
[Key]
public Guid LogID { get; set; }
[Required]
public string EventType { get; set; }
[Required]
public string TableName { get; set; }
public string ActionID { get; set; }
[Required]
public string RecordID { get; set; }
[Required]
public string ColumnName { get; set; }
public string OriginalValue { get; set; }
public string NewValue { get; set; }
[Required]
public string Created_by { get; set; }
[Required]
public DateTime Created_date { get; set; }
}
Related
I am using a unit of work to retrieve records / record form database, i am trying to implement some kind of a null object design pattern so that i dont have to check every-time if the returned object is null or not. I have tried searching online however i have not land on any good explanation on how to best achieve this in this current situation, I am familiar with the traditional approach for Null Object Design Pattern where you create a copy null class with hard coded properties and methods and return either the class or null-class based on outcome of the search in Db. however I feel that with the unit of work and repository patterns this approach is not valid. here is the class.
public class HR_Setup_Location
{
public string Street1 { get; set; }
public string Street2 { get; set; }
public string City { get; set; }
public string State { get; set; }
public string Country { get; set; }
public string FullAddress{
get { return $"{Street1} {City} {Country}"; }
}
[ForeignKey("Setup")]
public int SetupId { get; set; }
public virtual Setup Setup { get; set; }
public virtual ICollection<HR_Setup_OfficeEvent> HR_Setup_OfficeEvents { get; set; }
}
I tried the following , which is doing the job for now, however i appreciate your feedback on the approach if you have tried something similar in a similar situation. and what is the best way to address null objects in this pattern.
public interface ISetupLocationRepository : IRepository<HR_Setup_Location>
{
HR_Setup_Location GetById(int LocationId);
}
public class SetupLocationRepository : Repository<HR_Setup_Location>, ISetupLocationRepository
{
private readonly DataBaseContext context;
public SetupLocationRepository(DataBaseContext context)
: base(context)
{
this.context = context;
}
public HR_Setup_Location GetById(int LocationId)
{
HR_Setup_Location Obj = context.HR_Setup_Locations.Where(p => p.HR_Setup_LocationId == LocationId).FirstOrDefault();
if (Obj != null)
{
return Obj;
}
else
{
HR_Setup_Location Obj2 = new HR_Setup_Location()
{
HR_Setup_LocationId = -1,
Street1 = string.Empty,
Street2 = string.Empty,
City = string.Empty,
State = string.Empty,
Country = string.Empty,
SetupId = -1,
};
Obj2.HR_Setup_OfficeEvents = null;
return Obj2;
}
}
}
Then with the unit of work I am trying to access the location address by calling:
string LocationName = Vacancy.HR_Setup_LocationId.HasValue ? unitOfWork.SetupLocations.GetById(Vacancy.HR_Setup_LocationId.Value).FullAddress : "";
so basically if no id is based it will return an empty string, and if an id is passed but the record is no longer available in DataBase then the null object return empty for Fulladdress
I have a quality tracking webapp where I need each resolution steps to be signed by a user. To ease the process, I've built my model around following who filled the date at the end of each step.
My simplified Failure model looks like this:
public class failure {
[key]
public string FailureId { get; set; }
...
public int? OpenUserId { get; set; }
public int? DiagUserId { get; set; }
public int? CloseUserId { get; set; }
...
[ForeignKey("OpenUserId")]
public virtual signature OpenUser { get; set; }
[ForeignKey("DiagUserId")]
public virtual signature DiagUser { get; set; }
[ForeignKey("CloseUserId")]
public virtual signature CloseUser { get; set; }
}
and my Signature model:
public class signature {
[key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int SignatureId { get; set; }
public string UserName { get; set; }
[Column(TypeName:="Date")]
[DataType(DataType.Date)]
public date DateSign { get; set; }
}
The goal of this model is to minimize the number of signature in the table. So, if a user signs on multiple failure in one day, the code should only need to create one signature and reuse the same Id.
The problem arises when a user fills multiple steps in one save. Two or more signatures are created (which can be a problem in itself but it's not the focus right now) and an error is raised
Multiple added entities may have the same primary key.
because, before the SaveChanges, all Ids are at 0 and the code can't differentiate them.
Here's the POST:
async Task<ActionResult> Edit( FailureVM failure ) {
if (ModelState.IsValid) {
...
failure.OpenUserId = Await TryUpdateSignature(...);
failure.DiagUserId = Await TryUpdateSignature(...);
failure.CloseUserId = Await TryUpdateSignature(...);
...
await db.SaveChangesAsync;
}
}
and my function:
public static async Task<int?> TryUpdateSignature(MyDbContext db, Signature oldSignUser, Date? newDate, string userName)
{
int? SignatureID = null; //Returns null if no date
//Validate if there is a new date
if ((IsNothing(oldSignUser) && newDate != null) || (oldSignUser != null && oldSignUser.DateSign != newDate))
{
Signature recSignature = Await db.Signature.FirstOrDefaultAsync(s => s.UserID == userName && s.DateSign == newDate);
if (IsNothing(recSignature))
{
recSignature = new Signature;
recSignature.UserID = userName;
recSignature.DateSign = newDate;
db.Signature.Add(recSignature);
}
SignatureID = recSignature.SignatureID;
}
else if (oldSignUser != null && newDate != null)
{ //If no change, keep old signature
SignatureID = oldSignUser.SignatureID;
}
return SignatureID;
}
I tried using the object instead of the Ids but it didn't work. I could insert the Signature beforehand but I would prefer having everything saved at once.
At this point, I assume there's a problem with my model or my approach.
All your objects have Id's of 0 after going through the constructor, because 0 is the default int value. Because the PK has to identify the object in the database, EF won't even let you insert multiple objects with the same id values, let alone trying to save those back to the database, because it does not know how to identify each tuple (this even is of relevance to EF, not only DBMS for consistency reasons, because EF uses the PK values as default concurrency tokens).
The way to solve this is to give all your objects distinct PK values. How you do it is up to you - in my project, we've let all objects that are meant to be saved to the database from one common base class, let's call it BOBase:
public abstract class BOBase
{
public long Id { get; set; }
public BOBase()
{
Id = DbTempId.Next();
}
}
internal static class DbTempId
{
private static long _next = -1;
internal static long Next()
{
return _next--;
}
}
Once you let all your BO's inherit from this base class (and don't hide the inherited property), the Id's of newly created objects will be -1, -2 and so on. We even use this to differentiate between new and to be updated objects - new objects will have a negative Id, while already existing objects will have a positive one.
I cannot get the data from adc to update to database. I am using LINQ2SQL dbml.
I get no sql output from CAPcontext.Log, and Both tables have primary ID's
I feel it is an issue with the join in the LINQ query but couldnt find anything on the web regarding it. Can someone point out what am I doing wrong?
CAP_MDF_dataContextDataContext CAPcontext = new CAP_MDF_dataContextDataContext();
public bool LoadCAPadultdaycare()
{
CAPcontext.Log = Console.Out;
var adc = (from v in CAPcontext.AdultDayCares
join s in CAPcontext.States on v.state equals s.Name
select new CAPadultdaycare {
Avg = v.Avg,
City = v.city,
High = v.High,
Low = v.Low,
StateFullName = v.state,
StateAbbr = s.Code
}).ToList();
foreach (var item in adc)
{ item.City = "some city";
// updating more fields but omitted here
}
CAPcontext.SubmitChanges();
return true;
}
My CAPadultdaycare class is...
public class CAPadultdaycare
{
public decimal? High { get; set; }
public decimal? Low { get; set; }
public decimal? Avg { get; set; }
public string Zip { get; set; }
public string City { get; set; }
public string StateAbbr { get; set; }
public string StateFullName { get; set; }
}
You are creating new CAPadultdaycare objects, which are not attached to your data Context and hence not submitted.
The following may work
var adc = (from v in CAPcontext.AdultDayCares
join s in CAPcontext.States on v.state equals s.Name
select v).ToList();
foreach (var item in adc)
{ item.City = "some city";
// updating more fields but omitted here
}
CAPcontext.SubmitChanges();
but this means you are pulling all columns from your database.
I want to implement this simple scenario ,which I though EF will support out of the box.
I have a parent record named (Skill) and I am adding child records named (LinktoKB) to it. Now after adding a new LinktoKB, I want to return a view containing the up-to-date list of LinkToKBs (inclusing the newly added one).
Now my Post action method to add new LinktoKB is :-
[HttpPost]
[ValidateAntiForgeryToken]
[CheckUserPermissions(Action = "Edit", Model = "Skill")]
public async Task<ActionResult> AddKBLink(AssignKBLinksToSkill assignkblinkToSkill)
{
try
{
if (assignkblinkToSkill.LinkToKB == null)
{
return HttpNotFound();
}
if (ModelState.IsValid)
{
unitofwork.SkillRepository.AddKBLinkToSkill(assignkblinkToSkill, unitofwork.StaffRepository.GetLoginUserName(User.Identity.Name));
await unitofwork.Save();
//i have removed the values from the model state to prevent showing validation error "that the URL and name is required after succfully adding a new link"
// also to show the modified values and not the binded values
string oldlinkURL = assignkblinkToSkill.LinkToKB.URL;
ModelState.Clear();
var skillAfterAddingKBLink = await unitofwork.SkillRepository.FindSkill(assignkblinkToSkill.Skillid, r => r.LinkToKBs);
assignkblinkToSkill.LinktoKBList = skillAfterAddingKBLink.LinkToKBs.ToList(); //get the new lsit from DB after addign the new link
assignkblinkToSkill.LinkToKB.URL = "http://";//reset the values , so that user will not get old vlues
assignkblinkToSkill.LinkToKB.Name = String.Empty;
if (Request.IsAjaxRequest())
{
TempData["Partialmessage"] = string.Format("{0} URL have been Added", oldlinkURL);
return PartialView("AddKBLink", assignkblinkToSkill);
}
TempData["message"] = string.Format("{0} URL have been Added", oldlinkURL);
return View("AddKBLink", assignkblinkToSkill);
}
}
And my repository methods are:-
public async Task<Skill> FindSkill(int id, params Expression<Func<Skill, object>>[] includeProperties)
{
var query = context.Skills.AsQueryable();
if (includeProperties != null || includeProperties.Count() != 0 || includeProperties[0].Name == "0")
query = includeProperties.Aggregate(query, (current, include) => current.Include(include));
return await query.SingleOrDefaultAsync(a => a.SkillID == id);
}
&
public void AddKBLinkToSkill(AssignKBLinksToSkill assignKBLinkToSkill,string username)
{
var skill = context.Skills.SingleOrDefault(a=>a.SkillID == assignKBLinkToSkill.Skillid);
skill.LinkToKBs.Add(assignKBLinkToSkill.LinkToKB);
skill.Modified = System.DateTime.Now;
skill.ModifiedBy = staffrepo.GetUserIdByUserName(username);
context.Entry(skill).State = EntityState.Modified;
}
Currently I am getting a very strange behavior is that , the list that is returned to the view will not contain the newly added LinkToKB value and it will be replaced by the following value:-
assignkblinkToSkill.LinkToKB.URL = "http://"
so can anyone advice on this please, although I am explicitly retrieving the LinkToKB list from database?
visual studio will how the following at two different stages:-
First this is the newly added LinkToKB:-
Second EF have replace it with the value inside the action method:-
I spend the whole day trying to understand what is going on ... and if i removed these lines:-
assignkblinkToSkill.LinkToKB.URL = "http://";//reset the values , so that user will not get old vlues
assignkblinkToSkill.LinkToKB.Name = String.Empty;
i will get the new up-to-date list correctly (but i need them)..
I have two model classes (Skill & LinktoKB):-
public partial class Skill
{
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors")]
public Skill()
{
this.SkillLevels = new HashSet<SkillLevel>();
this.SkillLevelStaffs = new HashSet<SkillLevelStaff>();
this.Customers = new HashSet<Customer>();
this.LinkToKBs = new HashSet<LinkToKB>();
this.SkillVersionHistories = new HashSet<SkillVersionHistory>();
this.Skill1 = new HashSet<Skill>();
this.Skills = new HashSet<Skill>();
}
public int SkillID { get; set; }
public string Name { get; set; }
//code goes here
public virtual SkillStatu SkillStatu { get; set; }
public virtual SkillType SkillType { get; set; }
public virtual ICollection<LinkToKB> LinkToKBs { get; set; }
}
{
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors")]
public LinkToKB()
{
this.Skills = new HashSet<Skill>();
}
public int LinkToKBID { get; set; }
public string URL { get; set; }
public string Name { get; set; }
public virtual ICollection<Skill> Skills { get; set; }
}
and the following viewModel class:-
public class AssignKBLinksToSkill
{
public ICollection<LinkToKB> LinktoKBList { set; get; }
public LinkToKB LinkToKB { set; get; }
public int Skillid { set; get; }
}
In your code there's always one assignkblinkToSkill.LinkToKB instance. When it enters the method it's got some value that you store in the database. Later you re-assign its value to be "http://".
But this is still the instance that you added to the list skillAfterAddingKBLink.LinkToKBs!
You only have to create a new instance in the view model:
assignkblinkToSkill.LinkToKB = new LinkToKB();
assignkblinkToSkill.LinkToKB.URL = "http://";
Hey everyone, I'm getting killed on database queries in my MVC3 app when I'm mapping my repository to a View Model. The View is fairly complicated, the main object is Assignment but the View returns data from a few of it's relationships too. The View Mode looks like this
public int Id { get; set; }
public DateTime? RelevantDate { get; set; }
public String Name { get; set; }
public String ProcessName { get; set; }
public String Status { get; set; }
public String Assignee { get; set; }
public int AssigneeId { get; set; }
public bool HasAttachment { get; set; }
public bool IsGroupAssignee { get; set; }
public bool Overdue { get; set; }
public List<String> AvailableActions { get; set; }
public Dictionary<String, String> AssignmentData { get; set; }
public Dictionary<String, String> CompletionData { get; set; }
public List<Transactions> Comments { get; set; }
public List<Transactions> History { get; set; }
public List<File> Files { get; set; }
There's a lot going on there, but all of the data is relevant to the View. In my repository I explicitly load all of the required relationships with .Include (I'm using Entity Framework) but the data doesn't actually get loaded until I start iterating over the list.
var _assignments = (from ctx.Assignments
.Include("Process").Include("Files")
.Include("AssignmentDataSet")
.Include("Transactions")
.where w.Tenant.Id == _tenantId
select w);
In my controller I call a method on the repository that uses a query similar to this to get my data. A few variations but nothing too different from what's above.
Now, here's where I'm chewing up Database Transactions. I have to get this data into a ViewModel so I can display it.
private IList<AssignmentViewModel> CreateViewModel(IEnumerable<Assignment> aList)
{
var vList = new List<AssignmentViewModel>();
foreach (var a in aList)
{
var assigneeId = a.Assignee;
vList.Add(new AssignmentViewModel()
{
Id = a.Id,
AssigneeId = (int) a.Assignee,
HasAttachment = (a.Files.Count > 0),
Name = a.Name,
IsGroupAssignee = a.AssignedToGroup,
ProcessName = a.Process.Name,
RelevantDate = a.RelevantDate,
Status = a.Status,
AvailableActions = _assignmentRepository.GetAvailableActions(_user, a),
Assignee =
_users.Where(i => i.Id == assigneeId).Select(v => v.FullName).
FirstOrDefault(),
AssignmentData =
a.AssignmentDataSet.Where(st => st.State == "Assign").ToDictionary(
d => d.Name, d => d.Value),
CompletionData = a.AssignmentDataSet.Where(st => st.State == "Complete").ToDictionary(
d => d.Name, d => d.Value),
Comments = a.Transactions.Where(t => t.Action == "New Comment").ToList(),
History = a.Transactions.Where(t => t.Action != "New Comment").ToList(),
Overdue =
a.RelevantDate >= DateTime.UtcNow.AddHours(-5) || a.RelevantDate == null ||
a.Status == "Complete" || a.Status == "Canceled"
? false
: true
});
}
return vList;
}
This is resulting in approx 2.5 db queries per Assignment. This View will return a max of 30 results. That's a lot of hits to my database. Needless to say, the page is dead slow. Response times of 5-7 seconds. I'm embarrassed by it! Am I just trying to do too much here, or am I just doing it wrong?
How can I optimize this?
Two things I can see:
HasAttachment = (a.Files.Count > 0),
Just use Any() instead so you don't have to iterate over all files assuming this is still an IEnumerable:
HasAttachment = a.Files.Any(),
The other thing is comments and history:
Comments = a.Transactions.Where(t => t.Action == "New Comment").ToList(),
History = a.Transactions.Where(t => t.Action != "New Comment").ToList(),
You can combine these calls by materializing the full list before you create the AssignmentViewModel and then just take the appropriate parts:
var transactions = a.Transactions.ToList();
vList.Add(new AssignmentViewModel()
{
..
Comments = transactions.Where(t => t.Action == "New Comment").ToList(),
History = transactions.Where(t => t.Action != "New Comment").ToList(),
}
If you have to support this way of displaying your data you should consider a view and a corresponding entity in the database that selects all or most of the appropriate data for you using joins, so the effort you have to spend on bringing the data to the UI is much less.