I have this following code:
if (await TryUpdateModelAsync<Host>(
hostToUpdate,
"Host",
s => s.Name, s => s.Description, s => s.Address, s => s.Postcode, s => s.Suburb,
s => s.State))
{
try
{
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
catch (DbUpdateConcurrencyException ex)
{
var exceptionEntry = ex.Entries.Single();
var clientValues = (Host)exceptionEntry.Entity;
var databaseEntry = exceptionEntry.GetDatabaseValues();
if (databaseEntry == null)
{
ModelState.AddModelError(string.Empty, "Unable to save. " +
"The host was deleted by another user.");
return Page();
}
var dbValues = (Host)databaseEntry.ToObject();
await setDbErrorMessage(dbValues, clientValues, _context);
// Save the current RowVersion so next postback
// matches unless an new concurrency issue happens.
Host.RowVersion = (byte[])dbValues.RowVersion;
// Must clear the model error for the next postback.
ModelState.Remove("Host.RowVersion");
}
}
I have a properties for Host called: LastDateModified
and LastModified
which is calculated/predefined value
ie DateTime.Now
for LastDateModified
and _userManager.GetUserId(User)
for LastDateModifiedBy
.
So how do I pass this into this code?
await TryUpdateModelAsync<Host>(
hostToUpdate,
"Host",
s => s.Name, s => s.Description, s => s.Address, s => s.Postcode, s => s.Suburb,
s => s.State)
You can set the (alternative) value prior to saving the object:
var hostToUpdate = await _context.Host.FindAsync(s => s.Id == id);
if (await TryUpdateModelAsync(
hostToUpdate,
"host", // empty with MVC
s => s.Name, s => s.Description, s => s.Address,
s => s.Postcode, s => s.Suburb, s => s.State))
{
try
{
hostToUpdate.LastModified = DateTime.Now;
hostToUpdate.LastDateModifiedBy = _userManager.GetUserId(User);
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
// ...
}
Please note that LastModified
and LastDateModifiedBy
are not part of the TryUpdateModelAsync
statement. But if they were, the values will be overwritten by the action.
From the Razor Pages documentation:
The DB context keeps track of whether entities in memory are in sync with their corresponding rows in the DB. The DB context sync information determines what happens when SaveChangesAsync is called.
From the Mvc documentation (no longer updated):
The Entity Framework's automatic change tracking sets the Modified flag on the fields that are changed by form input. When the SaveChanges method is called, the Entity Framework creates SQL statements to update the database row.
To explain why this works, first TryUpdateModelAsync updates the fields updated by the user and then the action updates the additional fields. All are tracked and saved by the Entity Framework. This is default Entity Framework behaviour.
As a side note, an alternative for you would be to add code that automatically updates the fields. In that case you can't forget to set them and you save a few lines of code. And you won't have to change your code at all.
The strategy is that the entity implements the base fields and updates them on save changes. Here's a more extended version:
public interface IBaseEntity
{
DateTime LastDateModified { get; set; }
string LastDateModifiedBy { get; set; }
DateTime DateCreated { get; set; }
string DateCreatedBy { get; set; }
}
public class Host : IBaseEntity
{
// the other fields
// ...
public DateTime LastDateModified { get; set; }
public string LastDateModifiedBy { get; set; }
public DateTime DateCreated { get; set; }
public string DateCreatedBy { get; set; }
}
The context:
public partial class MyContext : DbContext
{
// Reference to the name of the current user.
private readonly string _userName;
public MyContext(DbContextOptions<MyContext> options, IHttpContextAccessor httpContext)
: base(options)
{
// Save the name of the current user so MyContext knows
// who it is. The advantage is that you won't need to lookup
// the User on each save changes.
_userName = httpContext.HttpContext.User.Identity.Name;
}
public virtual DbSet<Host> Host { get; set; }
// You'll only need to override this, unless you are
// also using non-async SaveChanges.
public override Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default(CancellationToken))
{
UpdateEntries();
return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
}
// You can move this to another partial class.
private void UpdateEntries()
{
// Modified
var modified = ChangeTracker.Entries().Where(v => v.State == EntityState.Modified && typeof(IBaseEntity).IsAssignableFrom(v.Entity.GetType())).ToList();
modified.ForEach(entry =>
{
((IBaseEntity)entry.Entity).LastDateModified = DateTime.UtcNow;
((IBaseEntity)entry.Entity).LastDateModifiedBy = _userName;
});
// Added
var added = ChangeTracker.Entries().Where(v => v.State == EntityState.Added && typeof(IBaseEntity).IsAssignableFrom(v.Entity.GetType())).ToList();
added.ForEach(entry =>
{
((IBaseEntity)entry.Entity).DateCreated = DateTime.UtcNow;
((IBaseEntity)entry.Entity).DateCreatedBy = _userName;
((IBaseEntity)entry.Entity).LastDateModified = DateTime.UtcNow;
((IBaseEntity)entry.Entity).LastDateModifiedBy = _userName;
});
}
// ...
}
Add HttpContextAccessor in Startup:
public void ConfigureServices(IServiceCollection services)
{
services.AddHttpContextAccessor();
Now each time an object that implements IBaseEntity is saved, the fields are automatically updated.
Please note that I didn't inject UserManager here. If the User contains a name claim then you can use that instead. This will save a call to the database.
As an improvement you can write a new service that takes care of resolving the user name and inject that instead.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With