Different behaviour between bridging and standalone model/class












0















We are trying to streamline our automatic updating fields for DateCreated, CreatedBy, LastDateModified, LastModifiedBy, DateDeleted and DeletedBy and worked OK by adding routine OnBeforeSaving in the ApplicationDBContext.cs and we also do the SOFT-DELETE for retaining the records (flagged as IsDeleted approach instead) when we deleted:



ApplicationDBContext.cs -



using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using AthlosifyWebArchery.Models;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Linq.Expressions;
using Microsoft.AspNetCore.Http;
using System.Security.Claims;
using Microsoft.AspNetCore.Identity;

namespace AthlosifyWebArchery.Data
{
public class ApplicationDbContext : IdentityDbContext<ApplicationUser, ApplicationRole, string,
IdentityUserClaim<string>,
ApplicationUserRole, IdentityUserLogin<string>,
IdentityRoleClaim<string>, IdentityUserToken<string>>
{
private readonly IHttpContextAccessor _httpContextAccessor;

public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options,
IHttpContextAccessor httpContextAccessor
)
: base(options)
{
_httpContextAccessor = httpContextAccessor;
}

public DbSet<AthlosifyWebArchery.Models.TournamentBatchItem> TournamentBatchItem { get; set; }
public DbSet<AthlosifyWebArchery.Models.TournamentBatch> TournamentBatch { get; set; }

public virtual DbSet<AthlosifyWebArchery.Models.Host> Host { get; set; }

public DbSet<AthlosifyWebArchery.Models.HostApplicationUser> HostApplicationUser { get; set; }

public virtual DbSet<AthlosifyWebArchery.Models.Club> Club { get; set; }

public DbSet<AthlosifyWebArchery.Models.ClubApplicationUser> ClubApplicationUser { get; set; }


protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);

foreach (var entityType in builder.Model.GetEntityTypes())
{
// 1. Add the IsDeleted property
entityType.GetOrAddProperty("IsDeleted", typeof(bool));

// 2. Create the query filter

var parameter = Expression.Parameter(entityType.ClrType);

// EF.Property<bool>(post, "IsDeleted")
var propertyMethodInfo = typeof(EF).GetMethod("Property").MakeGenericMethod(typeof(bool));
var isDeletedProperty = Expression.Call(propertyMethodInfo, parameter, Expression.Constant("IsDeleted"));

// EF.Property<bool>(post, "IsDeleted") == false
BinaryExpression compareExpression = Expression.MakeBinary(ExpressionType.Equal, isDeletedProperty, Expression.Constant(false));

// post => EF.Property<bool>(post, "IsDeleted") == false
var lambda = Expression.Lambda(compareExpression, parameter);

builder.Entity(entityType.ClrType).HasQueryFilter(lambda);
}

// Many to Many relationship - HostApplicationUser
builder.Entity<HostApplicationUser>()
.HasKey(bc => new { bc.HostID, bc.Id });

builder.Entity<HostApplicationUser>()
.HasOne(bc => bc.Host)
.WithMany(b => b.HostApplicationUsers)
.HasForeignKey(bc => bc.HostID);

builder.Entity<HostApplicationUser>()
.HasOne(bc => bc.ApplicationUser)
.WithMany(c => c.HostApplicationUsers)
.HasForeignKey(bc => bc.Id);


// Many to Many relationship - ClubApplicationUser
builder.Entity<ClubApplicationUser>()
.HasKey(bc => new { bc.ClubID, bc.Id });

builder.Entity<ClubApplicationUser>()
.HasOne(bc => bc.Club)
.WithMany(b => b.ClubApplicationUsers)
.HasForeignKey(bc => bc.ClubID);

builder.Entity<ClubApplicationUser>()
.HasOne(bc => bc.ApplicationUser)
.WithMany(c => c.ClubApplicationUsers)
.HasForeignKey(bc => bc.Id);

// Many to Many relationship - ApplicationUserRole
builder.Entity<ApplicationUserRole>(userRole =>
{
userRole.HasKey(ur => new { ur.UserId, ur.RoleId });

userRole.HasOne(ur => ur.Role)
.WithMany(r => r.UserRoles)
.HasForeignKey(ur => ur.RoleId)
.IsRequired();

userRole.HasOne(ur => ur.User)
.WithMany(r => r.ApplicationUserRoles)
.HasForeignKey(ur => ur.UserId)
.IsRequired();
});
}

public override int SaveChanges(bool acceptAllChangesOnSuccess)
{
OnBeforeSaving();
return base.SaveChanges(acceptAllChangesOnSuccess);
}

public override Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default(CancellationToken))
{
OnBeforeSaving();
return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
}

private void OnBeforeSaving()
{
if (_httpContextAccessor.HttpContext != null)
{
var userName = _httpContextAccessor.HttpContext.User.Identity.Name;
var userId = _httpContextAccessor.HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier);

// 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).CreatedBy = userId;
((IBaseEntity)entry.Entity).LastDateModified = DateTime.UtcNow;
((IBaseEntity)entry.Entity).LastModifiedBy = userId;
});

// 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).LastModifiedBy = userId;
});

// Deleted
var deleted = ChangeTracker.Entries().Where(v => v.State == EntityState.Deleted &&
typeof(IBaseEntity).IsAssignableFrom(v.Entity.GetType())).ToList();

deleted.ForEach(entry =>
{
((IBaseEntity)entry.Entity).DateDeleted = DateTime.UtcNow;
((IBaseEntity)entry.Entity).DeletedBy = userId;
});

foreach (var entry in ChangeTracker.Entries()
.Where(e => e.State == EntityState.Deleted &&
e.Metadata.GetProperties().Any(x => x.Name == "IsDeleted")))
{
switch (entry.State)
{
case EntityState.Added:
entry.CurrentValues["IsDeleted"] = false;
break;
case EntityState.Deleted:
entry.State = EntityState.Modified;
entry.CurrentValues["IsDeleted"] = true;
break;
}
}
}
else
{
// DbInitializer kicks in
}
}
}
}


Worked OK with standalone model/class ie. Club, Host, ApplicationUser ** BUT not bridging model/class ie. **ClubApplicationUser and HostApplicationUser



So we have to do manually on this (AssignClub.cshtml.cs for instance) by adding manually for creating/deleting as you can see below:



// Removed the current one
var clubApplicationUserToRemove = await _context.ClubApplicationUser
.FirstOrDefaultAsync(m => m.Id == id.ToString()
&& m.ClubID == AssignClubUser.OriginalClubID);

clubApplicationUserToRemove.DateDeleted = DateTime.UtcNow;
clubApplicationUserToRemove.DeletedBy = userID;
_context.ClubApplicationUser.Remove(clubApplicationUserToRemove);


... the deleted.Count() below returning 0 ... and the same with creating etc:



// Deleted
var deleted = ChangeTracker.Entries().Where(v => v.State == EntityState.Deleted &&
typeof(IBaseEntity).IsAssignableFrom(v.Entity.GetType())).ToList();

deleted.ForEach(entry =>
{
((IBaseEntity)entry.Entity).DateDeleted = DateTime.UtcNow;
((IBaseEntity)entry.Entity).DeletedBy = userId;
});


AssignClub.cshtml.cs -



using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
using AthlosifyWebArchery.Data;
using AthlosifyWebArchery.Models;
using AthlosifyWebArchery.Pages.Administrators.TournamentBatches;
using Microsoft.AspNetCore.Identity;
using System.ComponentModel.DataAnnotations;
using static AthlosifyWebArchery.Pages.Administrators.Users.AssignClubUserModel;
using Microsoft.AspNetCore.Http;
using System.Security.Claims;

namespace AthlosifyWebArchery.Pages.Administrators.Users
{
//public class AssignClubUserModel : ClubNamePageModel
public class AssignClubUserModel : UserViewPageModel
{
private readonly AthlosifyWebArchery.Data.ApplicationDbContext _context;
private readonly IHttpContextAccessor _httpContextAccessor;

public AssignClubUserModel(AthlosifyWebArchery.Data.ApplicationDbContext context,
IHttpContextAccessor httpContextAccessor
)
{
_context = context;
_httpContextAccessor = httpContextAccessor;
}

public class AssignClubUserViewModel<ApplicationUser>
{
public string Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string UserName { get; set; }
public Guid ClubID { get; set; }
public Guid OriginalClubID { get; set; }
public byte RowVersion { get; set; }
}

[BindProperty]
public AssignClubUserViewModel<ApplicationUser> AssignClubUser { get; set; }

//public SelectList ClubNameSL { get; set; }

public async Task<IActionResult> OnGetAsync(Guid? id)
{
if (id == null)
return NotFound();

AssignClubUser = await _context.Users
.Include(u => u.ClubApplicationUsers)
.Where(t => t.Id == id.ToString())
.Select(t => new AssignClubUserViewModel<ApplicationUser>
{
Id = t.Id,
FirstName = t.FirstName,
LastName = t.LastName,
UserName = t.UserName,
ClubID = t.ClubApplicationUsers.ElementAt(0).ClubID,
OriginalClubID = t.ClubApplicationUsers.ElementAt(0).ClubID,
RowVersion = t.ClubApplicationUsers.ElementAt(0).RowVersion
}).SingleAsync();

if (AssignClubUser == null)
return NotFound();


// Use strongly typed data rather than ViewData.
//ClubNameSL = new SelectList(_context.Club, "ClubID", "Name");

PopulateClubsDropDownList(_context, AssignClubUser.ClubID);

return Page();
}

public async Task<IActionResult> OnPostAsync(Guid id)
{
if (!ModelState.IsValid)
return Page();

// 1st approach:
// Modify the bridge model directly

/*var clubApplicationUserToUpdate = await _context.ClubApplicationUser
.FirstOrDefaultAsync(m => m.Id == id.ToString()
&& m.ClubID == AssignClubUser.OriginalClubID);

if (clubApplicationUserToUpdate == null)
return await HandleDeletedUser();

_context.Entry(clubApplicationUserToUpdate)
.Property("RowVersion").OriginalValue = AssignClubUser.RowVersion;

// This slightly tweek for this particular
// As the modified Change Track is not triggered

//_context.Entry(clubApplicationUserToUpdate)
// .Property("LastDateModified").CurrentValue = DateTime.UtcNow;

//_context.Entry(clubApplicationUserToUpdate)
// .Property("LastModifiedBy").CurrentValue = _httpContextAccessor.HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier);

if (await TryUpdateModelAsync<ClubApplicationUser>(
clubApplicationUserToUpdate,
"AssignClubUser",
s => s.Id, s => s.ClubID))
{
try
{
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
catch (DbUpdateConcurrencyException ex)
{
var exceptionEntry = ex.Entries.Single();
var clientValues = (ClubApplicationUser)exceptionEntry.Entity;
var databaseEntry = exceptionEntry.GetDatabaseValues();
if (databaseEntry == null)
{
ModelState.AddModelError(string.Empty, "Unable to save. " +
"The club application user was deleted by another user.");
return Page();
}

var dbValues = (ClubApplicationUser)databaseEntry.ToObject();
await setDbErrorMessage(dbValues, clientValues, _context);

AssignClubUser.RowVersion = (byte)dbValues.RowVersion;

ModelState.Remove("User.RowVersion");
}
}
*/

// 2nd approach:
// Soft -Delete and Add
// Did the soft-deleting and managed to add a new one BUT then die the roll back (adding the old one)
// Result: Violation of PRIMARY KEY constraint 'PK_ClubApplicationUser'.
// Cannot insert duplicate key in object
// Due to duplicate key

/*var clubApplicatonUserToRemove = await _context.ClubApplicationUser
.FirstOrDefaultAsync(m => m.Id == id.ToString());
ClubApplicationUser clubApplicatonUserToAdd = new ClubApplicationUser();
clubApplicatonUserToAdd.Id = id.ToString();
clubApplicatonUserToAdd.ClubID = AssignClubUser.SelectedClubID;


//_context.Entry(clubApplicatonUserToRemove)
// .Property("RowVersion").OriginalValue = User.RowVersion;

if (clubApplicatonUserToRemove != null)
{
_context.ClubApplicationUser.Remove(clubApplicatonUserToRemove);
await _context.SaveChangesAsync();clubApplicationUserDeleted
_context.ClubApplicationUser.Add(clubApplicatonUserToAdd);
await _context.SaveChangesAsync();
}*/

//delete all club memberships and add new one

//var clubApplicationUserDeleted = await _context.ClubApplicationUser
// .FirstOrDefaultAsync(m => m.Id == id.ToString()
// && m.ClubID == AssignClubUser.ClubID && m.IsDeleted == );

var userID = _httpContextAccessor.HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier);

if (AssignClubUser.ClubID != AssignClubUser.OriginalClubID)
{
var deletedClubApplicationUsers = _context.ClubApplicationUser.IgnoreQueryFilters()
.Where(post => post.Id == id.ToString()
&& post.ClubID == AssignClubUser.ClubID && EF.Property<bool>(post, "IsDeleted") == true);

if (deletedClubApplicationUsers.Count() > 0)
{
// Undo the deleted one
foreach (var deletedClubApplicationUser in deletedClubApplicationUsers)
{
var postEntry = _context.ChangeTracker.Entries<ClubApplicationUser>().First(entry => entry.Entity == deletedClubApplicationUser);
postEntry.Property("IsDeleted").CurrentValue = false;
postEntry.Property("LastDateModified").CurrentValue = DateTime.UtcNow;
postEntry.Property("LastModifiedBy").CurrentValue = userID;
postEntry.Property("DateDeleted").CurrentValue = null;
postEntry.Property("DeletedBy").CurrentValue = null;
}

// Removed the current one
var clubApplicationUserToRemove = await _context.ClubApplicationUser
.FirstOrDefaultAsync(m => m.Id == id.ToString()
&& m.ClubID == AssignClubUser.OriginalClubID);

clubApplicationUserToRemove.DateDeleted = DateTime.UtcNow;
clubApplicationUserToRemove.DeletedBy = userID;
_context.ClubApplicationUser.Remove(clubApplicationUserToRemove);

try
{
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
catch
{
PopulateClubsDropDownList(_context, AssignClubUser.ClubID);
return Page();
}
}
else
{
// Removed the current one
var clubApplicationUserToRemove = await _context.ClubApplicationUser
.FirstOrDefaultAsync(m => m.Id == id.ToString()
&& m.ClubID == AssignClubUser.OriginalClubID);
clubApplicationUserToRemove.DateDeleted = DateTime.UtcNow;
clubApplicationUserToRemove.DeletedBy = userID;
_context.ClubApplicationUser.Remove(clubApplicationUserToRemove);

try
{
_context.Entry(clubApplicationUserToRemove).State = EntityState.Deleted;
await _context.SaveChangesAsync();
}
catch
{
PopulateClubsDropDownList(_context, AssignClubUser.ClubID);
return Page();
}

// Added the new one
var newClubApplicationUser = new ClubApplicationUser()
{
Id = id.ToString(),
ClubID = AssignClubUser.ClubID,
DateCreated = DateTime.UtcNow,
CreatedBy = userID,
LastDateModified = DateTime.UtcNow,
LastModifiedBy = userID
};
_context.ClubApplicationUser.Add(newClubApplicationUser);

try
{
_context.Entry(newClubApplicationUser).State = EntityState.Added;
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
catch
{
PopulateClubsDropDownList(_context, AssignClubUser.ClubID);
return Page();
}
}
}

return RedirectToPage("./Index");
}

private async Task<IActionResult> HandleDeletedUser()
{
ClubApplicationUser deletedClubApplicationUser = new ClubApplicationUser();
ModelState.AddModelError(string.Empty,
"Unable to save. The club was deleted by another user.");
return Page();
}

private async Task setDbErrorMessage(ClubApplicationUser dbValues,
ClubApplicationUser clientValues, ApplicationDbContext context)
{
ModelState.AddModelError(string.Empty,
"The record you attempted to edit "
+ "was modified by another user after you. The "
+ "edit operation was canceled and the current values in the database "
+ "have been displayed. If you still want to edit this record, click "
+ "the Save button again.");
}

}
}


ClubApplicationUser.cs model:



using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Threading.Tasks;

namespace AthlosifyWebArchery.Models
{
public class ClubApplicationUser
{


public Guid ClubID { get; set; }

public Club Club { get; set; }

public string Id { get; set; }

public ApplicationUser ApplicationUser { get; set; }

public DateTime DateCreated { get; set; }

public string CreatedBy { get; set; }

public DateTime LastDateModified { get; set; }

public string LastModifiedBy { get; set; }

public DateTime? DateDeleted { get; set; }

public string DeletedBy { get; set; }

public bool IsDeleted { get; set; }

[Timestamp]
public byte RowVersion { get; set; }

[ForeignKey("CreatedBy")]
public ApplicationUser ClubApplicationCreatedUser { get; set; }

[ForeignKey("LastModifiedBy")]
public ApplicationUser ClubApplicationLastModifiedUser { get; set; }



}
}


Any ideas? The current solution is working OK BUT it just has extra routines to add these DateCreated, CreatedBy, LastDateModified, LastModifiedBy, DateDeleted and DeletedBy manually for this bridging model/class.



Environment:




  • .NET Core 2.2

  • SQL Server


Updates 1:



We even add this before the saving and it didn't trigger the automatic ChangeTracker.Entries():



_context.Entry(newClubApplicationUser).State = EntityState.Deleted;
await _context.SaveChangesAsync();


Updates 2:



Added the ClubApplicationUser model above.










share|improve this question

























  • What is the defination for model? It would be helpful if you could share us a mini demo to reproduce your issue.

    – Tao Zhou
    Jan 14 at 5:35











  • @TaoZhou I've added the ClubApplicationUser model as per request.

    – dcpartners
    Jan 18 at 21:46
















0















We are trying to streamline our automatic updating fields for DateCreated, CreatedBy, LastDateModified, LastModifiedBy, DateDeleted and DeletedBy and worked OK by adding routine OnBeforeSaving in the ApplicationDBContext.cs and we also do the SOFT-DELETE for retaining the records (flagged as IsDeleted approach instead) when we deleted:



ApplicationDBContext.cs -



using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using AthlosifyWebArchery.Models;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Linq.Expressions;
using Microsoft.AspNetCore.Http;
using System.Security.Claims;
using Microsoft.AspNetCore.Identity;

namespace AthlosifyWebArchery.Data
{
public class ApplicationDbContext : IdentityDbContext<ApplicationUser, ApplicationRole, string,
IdentityUserClaim<string>,
ApplicationUserRole, IdentityUserLogin<string>,
IdentityRoleClaim<string>, IdentityUserToken<string>>
{
private readonly IHttpContextAccessor _httpContextAccessor;

public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options,
IHttpContextAccessor httpContextAccessor
)
: base(options)
{
_httpContextAccessor = httpContextAccessor;
}

public DbSet<AthlosifyWebArchery.Models.TournamentBatchItem> TournamentBatchItem { get; set; }
public DbSet<AthlosifyWebArchery.Models.TournamentBatch> TournamentBatch { get; set; }

public virtual DbSet<AthlosifyWebArchery.Models.Host> Host { get; set; }

public DbSet<AthlosifyWebArchery.Models.HostApplicationUser> HostApplicationUser { get; set; }

public virtual DbSet<AthlosifyWebArchery.Models.Club> Club { get; set; }

public DbSet<AthlosifyWebArchery.Models.ClubApplicationUser> ClubApplicationUser { get; set; }


protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);

foreach (var entityType in builder.Model.GetEntityTypes())
{
// 1. Add the IsDeleted property
entityType.GetOrAddProperty("IsDeleted", typeof(bool));

// 2. Create the query filter

var parameter = Expression.Parameter(entityType.ClrType);

// EF.Property<bool>(post, "IsDeleted")
var propertyMethodInfo = typeof(EF).GetMethod("Property").MakeGenericMethod(typeof(bool));
var isDeletedProperty = Expression.Call(propertyMethodInfo, parameter, Expression.Constant("IsDeleted"));

// EF.Property<bool>(post, "IsDeleted") == false
BinaryExpression compareExpression = Expression.MakeBinary(ExpressionType.Equal, isDeletedProperty, Expression.Constant(false));

// post => EF.Property<bool>(post, "IsDeleted") == false
var lambda = Expression.Lambda(compareExpression, parameter);

builder.Entity(entityType.ClrType).HasQueryFilter(lambda);
}

// Many to Many relationship - HostApplicationUser
builder.Entity<HostApplicationUser>()
.HasKey(bc => new { bc.HostID, bc.Id });

builder.Entity<HostApplicationUser>()
.HasOne(bc => bc.Host)
.WithMany(b => b.HostApplicationUsers)
.HasForeignKey(bc => bc.HostID);

builder.Entity<HostApplicationUser>()
.HasOne(bc => bc.ApplicationUser)
.WithMany(c => c.HostApplicationUsers)
.HasForeignKey(bc => bc.Id);


// Many to Many relationship - ClubApplicationUser
builder.Entity<ClubApplicationUser>()
.HasKey(bc => new { bc.ClubID, bc.Id });

builder.Entity<ClubApplicationUser>()
.HasOne(bc => bc.Club)
.WithMany(b => b.ClubApplicationUsers)
.HasForeignKey(bc => bc.ClubID);

builder.Entity<ClubApplicationUser>()
.HasOne(bc => bc.ApplicationUser)
.WithMany(c => c.ClubApplicationUsers)
.HasForeignKey(bc => bc.Id);

// Many to Many relationship - ApplicationUserRole
builder.Entity<ApplicationUserRole>(userRole =>
{
userRole.HasKey(ur => new { ur.UserId, ur.RoleId });

userRole.HasOne(ur => ur.Role)
.WithMany(r => r.UserRoles)
.HasForeignKey(ur => ur.RoleId)
.IsRequired();

userRole.HasOne(ur => ur.User)
.WithMany(r => r.ApplicationUserRoles)
.HasForeignKey(ur => ur.UserId)
.IsRequired();
});
}

public override int SaveChanges(bool acceptAllChangesOnSuccess)
{
OnBeforeSaving();
return base.SaveChanges(acceptAllChangesOnSuccess);
}

public override Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default(CancellationToken))
{
OnBeforeSaving();
return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
}

private void OnBeforeSaving()
{
if (_httpContextAccessor.HttpContext != null)
{
var userName = _httpContextAccessor.HttpContext.User.Identity.Name;
var userId = _httpContextAccessor.HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier);

// 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).CreatedBy = userId;
((IBaseEntity)entry.Entity).LastDateModified = DateTime.UtcNow;
((IBaseEntity)entry.Entity).LastModifiedBy = userId;
});

// 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).LastModifiedBy = userId;
});

// Deleted
var deleted = ChangeTracker.Entries().Where(v => v.State == EntityState.Deleted &&
typeof(IBaseEntity).IsAssignableFrom(v.Entity.GetType())).ToList();

deleted.ForEach(entry =>
{
((IBaseEntity)entry.Entity).DateDeleted = DateTime.UtcNow;
((IBaseEntity)entry.Entity).DeletedBy = userId;
});

foreach (var entry in ChangeTracker.Entries()
.Where(e => e.State == EntityState.Deleted &&
e.Metadata.GetProperties().Any(x => x.Name == "IsDeleted")))
{
switch (entry.State)
{
case EntityState.Added:
entry.CurrentValues["IsDeleted"] = false;
break;
case EntityState.Deleted:
entry.State = EntityState.Modified;
entry.CurrentValues["IsDeleted"] = true;
break;
}
}
}
else
{
// DbInitializer kicks in
}
}
}
}


Worked OK with standalone model/class ie. Club, Host, ApplicationUser ** BUT not bridging model/class ie. **ClubApplicationUser and HostApplicationUser



So we have to do manually on this (AssignClub.cshtml.cs for instance) by adding manually for creating/deleting as you can see below:



// Removed the current one
var clubApplicationUserToRemove = await _context.ClubApplicationUser
.FirstOrDefaultAsync(m => m.Id == id.ToString()
&& m.ClubID == AssignClubUser.OriginalClubID);

clubApplicationUserToRemove.DateDeleted = DateTime.UtcNow;
clubApplicationUserToRemove.DeletedBy = userID;
_context.ClubApplicationUser.Remove(clubApplicationUserToRemove);


... the deleted.Count() below returning 0 ... and the same with creating etc:



// Deleted
var deleted = ChangeTracker.Entries().Where(v => v.State == EntityState.Deleted &&
typeof(IBaseEntity).IsAssignableFrom(v.Entity.GetType())).ToList();

deleted.ForEach(entry =>
{
((IBaseEntity)entry.Entity).DateDeleted = DateTime.UtcNow;
((IBaseEntity)entry.Entity).DeletedBy = userId;
});


AssignClub.cshtml.cs -



using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
using AthlosifyWebArchery.Data;
using AthlosifyWebArchery.Models;
using AthlosifyWebArchery.Pages.Administrators.TournamentBatches;
using Microsoft.AspNetCore.Identity;
using System.ComponentModel.DataAnnotations;
using static AthlosifyWebArchery.Pages.Administrators.Users.AssignClubUserModel;
using Microsoft.AspNetCore.Http;
using System.Security.Claims;

namespace AthlosifyWebArchery.Pages.Administrators.Users
{
//public class AssignClubUserModel : ClubNamePageModel
public class AssignClubUserModel : UserViewPageModel
{
private readonly AthlosifyWebArchery.Data.ApplicationDbContext _context;
private readonly IHttpContextAccessor _httpContextAccessor;

public AssignClubUserModel(AthlosifyWebArchery.Data.ApplicationDbContext context,
IHttpContextAccessor httpContextAccessor
)
{
_context = context;
_httpContextAccessor = httpContextAccessor;
}

public class AssignClubUserViewModel<ApplicationUser>
{
public string Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string UserName { get; set; }
public Guid ClubID { get; set; }
public Guid OriginalClubID { get; set; }
public byte RowVersion { get; set; }
}

[BindProperty]
public AssignClubUserViewModel<ApplicationUser> AssignClubUser { get; set; }

//public SelectList ClubNameSL { get; set; }

public async Task<IActionResult> OnGetAsync(Guid? id)
{
if (id == null)
return NotFound();

AssignClubUser = await _context.Users
.Include(u => u.ClubApplicationUsers)
.Where(t => t.Id == id.ToString())
.Select(t => new AssignClubUserViewModel<ApplicationUser>
{
Id = t.Id,
FirstName = t.FirstName,
LastName = t.LastName,
UserName = t.UserName,
ClubID = t.ClubApplicationUsers.ElementAt(0).ClubID,
OriginalClubID = t.ClubApplicationUsers.ElementAt(0).ClubID,
RowVersion = t.ClubApplicationUsers.ElementAt(0).RowVersion
}).SingleAsync();

if (AssignClubUser == null)
return NotFound();


// Use strongly typed data rather than ViewData.
//ClubNameSL = new SelectList(_context.Club, "ClubID", "Name");

PopulateClubsDropDownList(_context, AssignClubUser.ClubID);

return Page();
}

public async Task<IActionResult> OnPostAsync(Guid id)
{
if (!ModelState.IsValid)
return Page();

// 1st approach:
// Modify the bridge model directly

/*var clubApplicationUserToUpdate = await _context.ClubApplicationUser
.FirstOrDefaultAsync(m => m.Id == id.ToString()
&& m.ClubID == AssignClubUser.OriginalClubID);

if (clubApplicationUserToUpdate == null)
return await HandleDeletedUser();

_context.Entry(clubApplicationUserToUpdate)
.Property("RowVersion").OriginalValue = AssignClubUser.RowVersion;

// This slightly tweek for this particular
// As the modified Change Track is not triggered

//_context.Entry(clubApplicationUserToUpdate)
// .Property("LastDateModified").CurrentValue = DateTime.UtcNow;

//_context.Entry(clubApplicationUserToUpdate)
// .Property("LastModifiedBy").CurrentValue = _httpContextAccessor.HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier);

if (await TryUpdateModelAsync<ClubApplicationUser>(
clubApplicationUserToUpdate,
"AssignClubUser",
s => s.Id, s => s.ClubID))
{
try
{
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
catch (DbUpdateConcurrencyException ex)
{
var exceptionEntry = ex.Entries.Single();
var clientValues = (ClubApplicationUser)exceptionEntry.Entity;
var databaseEntry = exceptionEntry.GetDatabaseValues();
if (databaseEntry == null)
{
ModelState.AddModelError(string.Empty, "Unable to save. " +
"The club application user was deleted by another user.");
return Page();
}

var dbValues = (ClubApplicationUser)databaseEntry.ToObject();
await setDbErrorMessage(dbValues, clientValues, _context);

AssignClubUser.RowVersion = (byte)dbValues.RowVersion;

ModelState.Remove("User.RowVersion");
}
}
*/

// 2nd approach:
// Soft -Delete and Add
// Did the soft-deleting and managed to add a new one BUT then die the roll back (adding the old one)
// Result: Violation of PRIMARY KEY constraint 'PK_ClubApplicationUser'.
// Cannot insert duplicate key in object
// Due to duplicate key

/*var clubApplicatonUserToRemove = await _context.ClubApplicationUser
.FirstOrDefaultAsync(m => m.Id == id.ToString());
ClubApplicationUser clubApplicatonUserToAdd = new ClubApplicationUser();
clubApplicatonUserToAdd.Id = id.ToString();
clubApplicatonUserToAdd.ClubID = AssignClubUser.SelectedClubID;


//_context.Entry(clubApplicatonUserToRemove)
// .Property("RowVersion").OriginalValue = User.RowVersion;

if (clubApplicatonUserToRemove != null)
{
_context.ClubApplicationUser.Remove(clubApplicatonUserToRemove);
await _context.SaveChangesAsync();clubApplicationUserDeleted
_context.ClubApplicationUser.Add(clubApplicatonUserToAdd);
await _context.SaveChangesAsync();
}*/

//delete all club memberships and add new one

//var clubApplicationUserDeleted = await _context.ClubApplicationUser
// .FirstOrDefaultAsync(m => m.Id == id.ToString()
// && m.ClubID == AssignClubUser.ClubID && m.IsDeleted == );

var userID = _httpContextAccessor.HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier);

if (AssignClubUser.ClubID != AssignClubUser.OriginalClubID)
{
var deletedClubApplicationUsers = _context.ClubApplicationUser.IgnoreQueryFilters()
.Where(post => post.Id == id.ToString()
&& post.ClubID == AssignClubUser.ClubID && EF.Property<bool>(post, "IsDeleted") == true);

if (deletedClubApplicationUsers.Count() > 0)
{
// Undo the deleted one
foreach (var deletedClubApplicationUser in deletedClubApplicationUsers)
{
var postEntry = _context.ChangeTracker.Entries<ClubApplicationUser>().First(entry => entry.Entity == deletedClubApplicationUser);
postEntry.Property("IsDeleted").CurrentValue = false;
postEntry.Property("LastDateModified").CurrentValue = DateTime.UtcNow;
postEntry.Property("LastModifiedBy").CurrentValue = userID;
postEntry.Property("DateDeleted").CurrentValue = null;
postEntry.Property("DeletedBy").CurrentValue = null;
}

// Removed the current one
var clubApplicationUserToRemove = await _context.ClubApplicationUser
.FirstOrDefaultAsync(m => m.Id == id.ToString()
&& m.ClubID == AssignClubUser.OriginalClubID);

clubApplicationUserToRemove.DateDeleted = DateTime.UtcNow;
clubApplicationUserToRemove.DeletedBy = userID;
_context.ClubApplicationUser.Remove(clubApplicationUserToRemove);

try
{
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
catch
{
PopulateClubsDropDownList(_context, AssignClubUser.ClubID);
return Page();
}
}
else
{
// Removed the current one
var clubApplicationUserToRemove = await _context.ClubApplicationUser
.FirstOrDefaultAsync(m => m.Id == id.ToString()
&& m.ClubID == AssignClubUser.OriginalClubID);
clubApplicationUserToRemove.DateDeleted = DateTime.UtcNow;
clubApplicationUserToRemove.DeletedBy = userID;
_context.ClubApplicationUser.Remove(clubApplicationUserToRemove);

try
{
_context.Entry(clubApplicationUserToRemove).State = EntityState.Deleted;
await _context.SaveChangesAsync();
}
catch
{
PopulateClubsDropDownList(_context, AssignClubUser.ClubID);
return Page();
}

// Added the new one
var newClubApplicationUser = new ClubApplicationUser()
{
Id = id.ToString(),
ClubID = AssignClubUser.ClubID,
DateCreated = DateTime.UtcNow,
CreatedBy = userID,
LastDateModified = DateTime.UtcNow,
LastModifiedBy = userID
};
_context.ClubApplicationUser.Add(newClubApplicationUser);

try
{
_context.Entry(newClubApplicationUser).State = EntityState.Added;
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
catch
{
PopulateClubsDropDownList(_context, AssignClubUser.ClubID);
return Page();
}
}
}

return RedirectToPage("./Index");
}

private async Task<IActionResult> HandleDeletedUser()
{
ClubApplicationUser deletedClubApplicationUser = new ClubApplicationUser();
ModelState.AddModelError(string.Empty,
"Unable to save. The club was deleted by another user.");
return Page();
}

private async Task setDbErrorMessage(ClubApplicationUser dbValues,
ClubApplicationUser clientValues, ApplicationDbContext context)
{
ModelState.AddModelError(string.Empty,
"The record you attempted to edit "
+ "was modified by another user after you. The "
+ "edit operation was canceled and the current values in the database "
+ "have been displayed. If you still want to edit this record, click "
+ "the Save button again.");
}

}
}


ClubApplicationUser.cs model:



using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Threading.Tasks;

namespace AthlosifyWebArchery.Models
{
public class ClubApplicationUser
{


public Guid ClubID { get; set; }

public Club Club { get; set; }

public string Id { get; set; }

public ApplicationUser ApplicationUser { get; set; }

public DateTime DateCreated { get; set; }

public string CreatedBy { get; set; }

public DateTime LastDateModified { get; set; }

public string LastModifiedBy { get; set; }

public DateTime? DateDeleted { get; set; }

public string DeletedBy { get; set; }

public bool IsDeleted { get; set; }

[Timestamp]
public byte RowVersion { get; set; }

[ForeignKey("CreatedBy")]
public ApplicationUser ClubApplicationCreatedUser { get; set; }

[ForeignKey("LastModifiedBy")]
public ApplicationUser ClubApplicationLastModifiedUser { get; set; }



}
}


Any ideas? The current solution is working OK BUT it just has extra routines to add these DateCreated, CreatedBy, LastDateModified, LastModifiedBy, DateDeleted and DeletedBy manually for this bridging model/class.



Environment:




  • .NET Core 2.2

  • SQL Server


Updates 1:



We even add this before the saving and it didn't trigger the automatic ChangeTracker.Entries():



_context.Entry(newClubApplicationUser).State = EntityState.Deleted;
await _context.SaveChangesAsync();


Updates 2:



Added the ClubApplicationUser model above.










share|improve this question

























  • What is the defination for model? It would be helpful if you could share us a mini demo to reproduce your issue.

    – Tao Zhou
    Jan 14 at 5:35











  • @TaoZhou I've added the ClubApplicationUser model as per request.

    – dcpartners
    Jan 18 at 21:46














0












0








0








We are trying to streamline our automatic updating fields for DateCreated, CreatedBy, LastDateModified, LastModifiedBy, DateDeleted and DeletedBy and worked OK by adding routine OnBeforeSaving in the ApplicationDBContext.cs and we also do the SOFT-DELETE for retaining the records (flagged as IsDeleted approach instead) when we deleted:



ApplicationDBContext.cs -



using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using AthlosifyWebArchery.Models;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Linq.Expressions;
using Microsoft.AspNetCore.Http;
using System.Security.Claims;
using Microsoft.AspNetCore.Identity;

namespace AthlosifyWebArchery.Data
{
public class ApplicationDbContext : IdentityDbContext<ApplicationUser, ApplicationRole, string,
IdentityUserClaim<string>,
ApplicationUserRole, IdentityUserLogin<string>,
IdentityRoleClaim<string>, IdentityUserToken<string>>
{
private readonly IHttpContextAccessor _httpContextAccessor;

public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options,
IHttpContextAccessor httpContextAccessor
)
: base(options)
{
_httpContextAccessor = httpContextAccessor;
}

public DbSet<AthlosifyWebArchery.Models.TournamentBatchItem> TournamentBatchItem { get; set; }
public DbSet<AthlosifyWebArchery.Models.TournamentBatch> TournamentBatch { get; set; }

public virtual DbSet<AthlosifyWebArchery.Models.Host> Host { get; set; }

public DbSet<AthlosifyWebArchery.Models.HostApplicationUser> HostApplicationUser { get; set; }

public virtual DbSet<AthlosifyWebArchery.Models.Club> Club { get; set; }

public DbSet<AthlosifyWebArchery.Models.ClubApplicationUser> ClubApplicationUser { get; set; }


protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);

foreach (var entityType in builder.Model.GetEntityTypes())
{
// 1. Add the IsDeleted property
entityType.GetOrAddProperty("IsDeleted", typeof(bool));

// 2. Create the query filter

var parameter = Expression.Parameter(entityType.ClrType);

// EF.Property<bool>(post, "IsDeleted")
var propertyMethodInfo = typeof(EF).GetMethod("Property").MakeGenericMethod(typeof(bool));
var isDeletedProperty = Expression.Call(propertyMethodInfo, parameter, Expression.Constant("IsDeleted"));

// EF.Property<bool>(post, "IsDeleted") == false
BinaryExpression compareExpression = Expression.MakeBinary(ExpressionType.Equal, isDeletedProperty, Expression.Constant(false));

// post => EF.Property<bool>(post, "IsDeleted") == false
var lambda = Expression.Lambda(compareExpression, parameter);

builder.Entity(entityType.ClrType).HasQueryFilter(lambda);
}

// Many to Many relationship - HostApplicationUser
builder.Entity<HostApplicationUser>()
.HasKey(bc => new { bc.HostID, bc.Id });

builder.Entity<HostApplicationUser>()
.HasOne(bc => bc.Host)
.WithMany(b => b.HostApplicationUsers)
.HasForeignKey(bc => bc.HostID);

builder.Entity<HostApplicationUser>()
.HasOne(bc => bc.ApplicationUser)
.WithMany(c => c.HostApplicationUsers)
.HasForeignKey(bc => bc.Id);


// Many to Many relationship - ClubApplicationUser
builder.Entity<ClubApplicationUser>()
.HasKey(bc => new { bc.ClubID, bc.Id });

builder.Entity<ClubApplicationUser>()
.HasOne(bc => bc.Club)
.WithMany(b => b.ClubApplicationUsers)
.HasForeignKey(bc => bc.ClubID);

builder.Entity<ClubApplicationUser>()
.HasOne(bc => bc.ApplicationUser)
.WithMany(c => c.ClubApplicationUsers)
.HasForeignKey(bc => bc.Id);

// Many to Many relationship - ApplicationUserRole
builder.Entity<ApplicationUserRole>(userRole =>
{
userRole.HasKey(ur => new { ur.UserId, ur.RoleId });

userRole.HasOne(ur => ur.Role)
.WithMany(r => r.UserRoles)
.HasForeignKey(ur => ur.RoleId)
.IsRequired();

userRole.HasOne(ur => ur.User)
.WithMany(r => r.ApplicationUserRoles)
.HasForeignKey(ur => ur.UserId)
.IsRequired();
});
}

public override int SaveChanges(bool acceptAllChangesOnSuccess)
{
OnBeforeSaving();
return base.SaveChanges(acceptAllChangesOnSuccess);
}

public override Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default(CancellationToken))
{
OnBeforeSaving();
return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
}

private void OnBeforeSaving()
{
if (_httpContextAccessor.HttpContext != null)
{
var userName = _httpContextAccessor.HttpContext.User.Identity.Name;
var userId = _httpContextAccessor.HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier);

// 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).CreatedBy = userId;
((IBaseEntity)entry.Entity).LastDateModified = DateTime.UtcNow;
((IBaseEntity)entry.Entity).LastModifiedBy = userId;
});

// 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).LastModifiedBy = userId;
});

// Deleted
var deleted = ChangeTracker.Entries().Where(v => v.State == EntityState.Deleted &&
typeof(IBaseEntity).IsAssignableFrom(v.Entity.GetType())).ToList();

deleted.ForEach(entry =>
{
((IBaseEntity)entry.Entity).DateDeleted = DateTime.UtcNow;
((IBaseEntity)entry.Entity).DeletedBy = userId;
});

foreach (var entry in ChangeTracker.Entries()
.Where(e => e.State == EntityState.Deleted &&
e.Metadata.GetProperties().Any(x => x.Name == "IsDeleted")))
{
switch (entry.State)
{
case EntityState.Added:
entry.CurrentValues["IsDeleted"] = false;
break;
case EntityState.Deleted:
entry.State = EntityState.Modified;
entry.CurrentValues["IsDeleted"] = true;
break;
}
}
}
else
{
// DbInitializer kicks in
}
}
}
}


Worked OK with standalone model/class ie. Club, Host, ApplicationUser ** BUT not bridging model/class ie. **ClubApplicationUser and HostApplicationUser



So we have to do manually on this (AssignClub.cshtml.cs for instance) by adding manually for creating/deleting as you can see below:



// Removed the current one
var clubApplicationUserToRemove = await _context.ClubApplicationUser
.FirstOrDefaultAsync(m => m.Id == id.ToString()
&& m.ClubID == AssignClubUser.OriginalClubID);

clubApplicationUserToRemove.DateDeleted = DateTime.UtcNow;
clubApplicationUserToRemove.DeletedBy = userID;
_context.ClubApplicationUser.Remove(clubApplicationUserToRemove);


... the deleted.Count() below returning 0 ... and the same with creating etc:



// Deleted
var deleted = ChangeTracker.Entries().Where(v => v.State == EntityState.Deleted &&
typeof(IBaseEntity).IsAssignableFrom(v.Entity.GetType())).ToList();

deleted.ForEach(entry =>
{
((IBaseEntity)entry.Entity).DateDeleted = DateTime.UtcNow;
((IBaseEntity)entry.Entity).DeletedBy = userId;
});


AssignClub.cshtml.cs -



using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
using AthlosifyWebArchery.Data;
using AthlosifyWebArchery.Models;
using AthlosifyWebArchery.Pages.Administrators.TournamentBatches;
using Microsoft.AspNetCore.Identity;
using System.ComponentModel.DataAnnotations;
using static AthlosifyWebArchery.Pages.Administrators.Users.AssignClubUserModel;
using Microsoft.AspNetCore.Http;
using System.Security.Claims;

namespace AthlosifyWebArchery.Pages.Administrators.Users
{
//public class AssignClubUserModel : ClubNamePageModel
public class AssignClubUserModel : UserViewPageModel
{
private readonly AthlosifyWebArchery.Data.ApplicationDbContext _context;
private readonly IHttpContextAccessor _httpContextAccessor;

public AssignClubUserModel(AthlosifyWebArchery.Data.ApplicationDbContext context,
IHttpContextAccessor httpContextAccessor
)
{
_context = context;
_httpContextAccessor = httpContextAccessor;
}

public class AssignClubUserViewModel<ApplicationUser>
{
public string Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string UserName { get; set; }
public Guid ClubID { get; set; }
public Guid OriginalClubID { get; set; }
public byte RowVersion { get; set; }
}

[BindProperty]
public AssignClubUserViewModel<ApplicationUser> AssignClubUser { get; set; }

//public SelectList ClubNameSL { get; set; }

public async Task<IActionResult> OnGetAsync(Guid? id)
{
if (id == null)
return NotFound();

AssignClubUser = await _context.Users
.Include(u => u.ClubApplicationUsers)
.Where(t => t.Id == id.ToString())
.Select(t => new AssignClubUserViewModel<ApplicationUser>
{
Id = t.Id,
FirstName = t.FirstName,
LastName = t.LastName,
UserName = t.UserName,
ClubID = t.ClubApplicationUsers.ElementAt(0).ClubID,
OriginalClubID = t.ClubApplicationUsers.ElementAt(0).ClubID,
RowVersion = t.ClubApplicationUsers.ElementAt(0).RowVersion
}).SingleAsync();

if (AssignClubUser == null)
return NotFound();


// Use strongly typed data rather than ViewData.
//ClubNameSL = new SelectList(_context.Club, "ClubID", "Name");

PopulateClubsDropDownList(_context, AssignClubUser.ClubID);

return Page();
}

public async Task<IActionResult> OnPostAsync(Guid id)
{
if (!ModelState.IsValid)
return Page();

// 1st approach:
// Modify the bridge model directly

/*var clubApplicationUserToUpdate = await _context.ClubApplicationUser
.FirstOrDefaultAsync(m => m.Id == id.ToString()
&& m.ClubID == AssignClubUser.OriginalClubID);

if (clubApplicationUserToUpdate == null)
return await HandleDeletedUser();

_context.Entry(clubApplicationUserToUpdate)
.Property("RowVersion").OriginalValue = AssignClubUser.RowVersion;

// This slightly tweek for this particular
// As the modified Change Track is not triggered

//_context.Entry(clubApplicationUserToUpdate)
// .Property("LastDateModified").CurrentValue = DateTime.UtcNow;

//_context.Entry(clubApplicationUserToUpdate)
// .Property("LastModifiedBy").CurrentValue = _httpContextAccessor.HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier);

if (await TryUpdateModelAsync<ClubApplicationUser>(
clubApplicationUserToUpdate,
"AssignClubUser",
s => s.Id, s => s.ClubID))
{
try
{
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
catch (DbUpdateConcurrencyException ex)
{
var exceptionEntry = ex.Entries.Single();
var clientValues = (ClubApplicationUser)exceptionEntry.Entity;
var databaseEntry = exceptionEntry.GetDatabaseValues();
if (databaseEntry == null)
{
ModelState.AddModelError(string.Empty, "Unable to save. " +
"The club application user was deleted by another user.");
return Page();
}

var dbValues = (ClubApplicationUser)databaseEntry.ToObject();
await setDbErrorMessage(dbValues, clientValues, _context);

AssignClubUser.RowVersion = (byte)dbValues.RowVersion;

ModelState.Remove("User.RowVersion");
}
}
*/

// 2nd approach:
// Soft -Delete and Add
// Did the soft-deleting and managed to add a new one BUT then die the roll back (adding the old one)
// Result: Violation of PRIMARY KEY constraint 'PK_ClubApplicationUser'.
// Cannot insert duplicate key in object
// Due to duplicate key

/*var clubApplicatonUserToRemove = await _context.ClubApplicationUser
.FirstOrDefaultAsync(m => m.Id == id.ToString());
ClubApplicationUser clubApplicatonUserToAdd = new ClubApplicationUser();
clubApplicatonUserToAdd.Id = id.ToString();
clubApplicatonUserToAdd.ClubID = AssignClubUser.SelectedClubID;


//_context.Entry(clubApplicatonUserToRemove)
// .Property("RowVersion").OriginalValue = User.RowVersion;

if (clubApplicatonUserToRemove != null)
{
_context.ClubApplicationUser.Remove(clubApplicatonUserToRemove);
await _context.SaveChangesAsync();clubApplicationUserDeleted
_context.ClubApplicationUser.Add(clubApplicatonUserToAdd);
await _context.SaveChangesAsync();
}*/

//delete all club memberships and add new one

//var clubApplicationUserDeleted = await _context.ClubApplicationUser
// .FirstOrDefaultAsync(m => m.Id == id.ToString()
// && m.ClubID == AssignClubUser.ClubID && m.IsDeleted == );

var userID = _httpContextAccessor.HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier);

if (AssignClubUser.ClubID != AssignClubUser.OriginalClubID)
{
var deletedClubApplicationUsers = _context.ClubApplicationUser.IgnoreQueryFilters()
.Where(post => post.Id == id.ToString()
&& post.ClubID == AssignClubUser.ClubID && EF.Property<bool>(post, "IsDeleted") == true);

if (deletedClubApplicationUsers.Count() > 0)
{
// Undo the deleted one
foreach (var deletedClubApplicationUser in deletedClubApplicationUsers)
{
var postEntry = _context.ChangeTracker.Entries<ClubApplicationUser>().First(entry => entry.Entity == deletedClubApplicationUser);
postEntry.Property("IsDeleted").CurrentValue = false;
postEntry.Property("LastDateModified").CurrentValue = DateTime.UtcNow;
postEntry.Property("LastModifiedBy").CurrentValue = userID;
postEntry.Property("DateDeleted").CurrentValue = null;
postEntry.Property("DeletedBy").CurrentValue = null;
}

// Removed the current one
var clubApplicationUserToRemove = await _context.ClubApplicationUser
.FirstOrDefaultAsync(m => m.Id == id.ToString()
&& m.ClubID == AssignClubUser.OriginalClubID);

clubApplicationUserToRemove.DateDeleted = DateTime.UtcNow;
clubApplicationUserToRemove.DeletedBy = userID;
_context.ClubApplicationUser.Remove(clubApplicationUserToRemove);

try
{
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
catch
{
PopulateClubsDropDownList(_context, AssignClubUser.ClubID);
return Page();
}
}
else
{
// Removed the current one
var clubApplicationUserToRemove = await _context.ClubApplicationUser
.FirstOrDefaultAsync(m => m.Id == id.ToString()
&& m.ClubID == AssignClubUser.OriginalClubID);
clubApplicationUserToRemove.DateDeleted = DateTime.UtcNow;
clubApplicationUserToRemove.DeletedBy = userID;
_context.ClubApplicationUser.Remove(clubApplicationUserToRemove);

try
{
_context.Entry(clubApplicationUserToRemove).State = EntityState.Deleted;
await _context.SaveChangesAsync();
}
catch
{
PopulateClubsDropDownList(_context, AssignClubUser.ClubID);
return Page();
}

// Added the new one
var newClubApplicationUser = new ClubApplicationUser()
{
Id = id.ToString(),
ClubID = AssignClubUser.ClubID,
DateCreated = DateTime.UtcNow,
CreatedBy = userID,
LastDateModified = DateTime.UtcNow,
LastModifiedBy = userID
};
_context.ClubApplicationUser.Add(newClubApplicationUser);

try
{
_context.Entry(newClubApplicationUser).State = EntityState.Added;
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
catch
{
PopulateClubsDropDownList(_context, AssignClubUser.ClubID);
return Page();
}
}
}

return RedirectToPage("./Index");
}

private async Task<IActionResult> HandleDeletedUser()
{
ClubApplicationUser deletedClubApplicationUser = new ClubApplicationUser();
ModelState.AddModelError(string.Empty,
"Unable to save. The club was deleted by another user.");
return Page();
}

private async Task setDbErrorMessage(ClubApplicationUser dbValues,
ClubApplicationUser clientValues, ApplicationDbContext context)
{
ModelState.AddModelError(string.Empty,
"The record you attempted to edit "
+ "was modified by another user after you. The "
+ "edit operation was canceled and the current values in the database "
+ "have been displayed. If you still want to edit this record, click "
+ "the Save button again.");
}

}
}


ClubApplicationUser.cs model:



using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Threading.Tasks;

namespace AthlosifyWebArchery.Models
{
public class ClubApplicationUser
{


public Guid ClubID { get; set; }

public Club Club { get; set; }

public string Id { get; set; }

public ApplicationUser ApplicationUser { get; set; }

public DateTime DateCreated { get; set; }

public string CreatedBy { get; set; }

public DateTime LastDateModified { get; set; }

public string LastModifiedBy { get; set; }

public DateTime? DateDeleted { get; set; }

public string DeletedBy { get; set; }

public bool IsDeleted { get; set; }

[Timestamp]
public byte RowVersion { get; set; }

[ForeignKey("CreatedBy")]
public ApplicationUser ClubApplicationCreatedUser { get; set; }

[ForeignKey("LastModifiedBy")]
public ApplicationUser ClubApplicationLastModifiedUser { get; set; }



}
}


Any ideas? The current solution is working OK BUT it just has extra routines to add these DateCreated, CreatedBy, LastDateModified, LastModifiedBy, DateDeleted and DeletedBy manually for this bridging model/class.



Environment:




  • .NET Core 2.2

  • SQL Server


Updates 1:



We even add this before the saving and it didn't trigger the automatic ChangeTracker.Entries():



_context.Entry(newClubApplicationUser).State = EntityState.Deleted;
await _context.SaveChangesAsync();


Updates 2:



Added the ClubApplicationUser model above.










share|improve this question
















We are trying to streamline our automatic updating fields for DateCreated, CreatedBy, LastDateModified, LastModifiedBy, DateDeleted and DeletedBy and worked OK by adding routine OnBeforeSaving in the ApplicationDBContext.cs and we also do the SOFT-DELETE for retaining the records (flagged as IsDeleted approach instead) when we deleted:



ApplicationDBContext.cs -



using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using AthlosifyWebArchery.Models;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Linq.Expressions;
using Microsoft.AspNetCore.Http;
using System.Security.Claims;
using Microsoft.AspNetCore.Identity;

namespace AthlosifyWebArchery.Data
{
public class ApplicationDbContext : IdentityDbContext<ApplicationUser, ApplicationRole, string,
IdentityUserClaim<string>,
ApplicationUserRole, IdentityUserLogin<string>,
IdentityRoleClaim<string>, IdentityUserToken<string>>
{
private readonly IHttpContextAccessor _httpContextAccessor;

public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options,
IHttpContextAccessor httpContextAccessor
)
: base(options)
{
_httpContextAccessor = httpContextAccessor;
}

public DbSet<AthlosifyWebArchery.Models.TournamentBatchItem> TournamentBatchItem { get; set; }
public DbSet<AthlosifyWebArchery.Models.TournamentBatch> TournamentBatch { get; set; }

public virtual DbSet<AthlosifyWebArchery.Models.Host> Host { get; set; }

public DbSet<AthlosifyWebArchery.Models.HostApplicationUser> HostApplicationUser { get; set; }

public virtual DbSet<AthlosifyWebArchery.Models.Club> Club { get; set; }

public DbSet<AthlosifyWebArchery.Models.ClubApplicationUser> ClubApplicationUser { get; set; }


protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);

foreach (var entityType in builder.Model.GetEntityTypes())
{
// 1. Add the IsDeleted property
entityType.GetOrAddProperty("IsDeleted", typeof(bool));

// 2. Create the query filter

var parameter = Expression.Parameter(entityType.ClrType);

// EF.Property<bool>(post, "IsDeleted")
var propertyMethodInfo = typeof(EF).GetMethod("Property").MakeGenericMethod(typeof(bool));
var isDeletedProperty = Expression.Call(propertyMethodInfo, parameter, Expression.Constant("IsDeleted"));

// EF.Property<bool>(post, "IsDeleted") == false
BinaryExpression compareExpression = Expression.MakeBinary(ExpressionType.Equal, isDeletedProperty, Expression.Constant(false));

// post => EF.Property<bool>(post, "IsDeleted") == false
var lambda = Expression.Lambda(compareExpression, parameter);

builder.Entity(entityType.ClrType).HasQueryFilter(lambda);
}

// Many to Many relationship - HostApplicationUser
builder.Entity<HostApplicationUser>()
.HasKey(bc => new { bc.HostID, bc.Id });

builder.Entity<HostApplicationUser>()
.HasOne(bc => bc.Host)
.WithMany(b => b.HostApplicationUsers)
.HasForeignKey(bc => bc.HostID);

builder.Entity<HostApplicationUser>()
.HasOne(bc => bc.ApplicationUser)
.WithMany(c => c.HostApplicationUsers)
.HasForeignKey(bc => bc.Id);


// Many to Many relationship - ClubApplicationUser
builder.Entity<ClubApplicationUser>()
.HasKey(bc => new { bc.ClubID, bc.Id });

builder.Entity<ClubApplicationUser>()
.HasOne(bc => bc.Club)
.WithMany(b => b.ClubApplicationUsers)
.HasForeignKey(bc => bc.ClubID);

builder.Entity<ClubApplicationUser>()
.HasOne(bc => bc.ApplicationUser)
.WithMany(c => c.ClubApplicationUsers)
.HasForeignKey(bc => bc.Id);

// Many to Many relationship - ApplicationUserRole
builder.Entity<ApplicationUserRole>(userRole =>
{
userRole.HasKey(ur => new { ur.UserId, ur.RoleId });

userRole.HasOne(ur => ur.Role)
.WithMany(r => r.UserRoles)
.HasForeignKey(ur => ur.RoleId)
.IsRequired();

userRole.HasOne(ur => ur.User)
.WithMany(r => r.ApplicationUserRoles)
.HasForeignKey(ur => ur.UserId)
.IsRequired();
});
}

public override int SaveChanges(bool acceptAllChangesOnSuccess)
{
OnBeforeSaving();
return base.SaveChanges(acceptAllChangesOnSuccess);
}

public override Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default(CancellationToken))
{
OnBeforeSaving();
return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
}

private void OnBeforeSaving()
{
if (_httpContextAccessor.HttpContext != null)
{
var userName = _httpContextAccessor.HttpContext.User.Identity.Name;
var userId = _httpContextAccessor.HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier);

// 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).CreatedBy = userId;
((IBaseEntity)entry.Entity).LastDateModified = DateTime.UtcNow;
((IBaseEntity)entry.Entity).LastModifiedBy = userId;
});

// 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).LastModifiedBy = userId;
});

// Deleted
var deleted = ChangeTracker.Entries().Where(v => v.State == EntityState.Deleted &&
typeof(IBaseEntity).IsAssignableFrom(v.Entity.GetType())).ToList();

deleted.ForEach(entry =>
{
((IBaseEntity)entry.Entity).DateDeleted = DateTime.UtcNow;
((IBaseEntity)entry.Entity).DeletedBy = userId;
});

foreach (var entry in ChangeTracker.Entries()
.Where(e => e.State == EntityState.Deleted &&
e.Metadata.GetProperties().Any(x => x.Name == "IsDeleted")))
{
switch (entry.State)
{
case EntityState.Added:
entry.CurrentValues["IsDeleted"] = false;
break;
case EntityState.Deleted:
entry.State = EntityState.Modified;
entry.CurrentValues["IsDeleted"] = true;
break;
}
}
}
else
{
// DbInitializer kicks in
}
}
}
}


Worked OK with standalone model/class ie. Club, Host, ApplicationUser ** BUT not bridging model/class ie. **ClubApplicationUser and HostApplicationUser



So we have to do manually on this (AssignClub.cshtml.cs for instance) by adding manually for creating/deleting as you can see below:



// Removed the current one
var clubApplicationUserToRemove = await _context.ClubApplicationUser
.FirstOrDefaultAsync(m => m.Id == id.ToString()
&& m.ClubID == AssignClubUser.OriginalClubID);

clubApplicationUserToRemove.DateDeleted = DateTime.UtcNow;
clubApplicationUserToRemove.DeletedBy = userID;
_context.ClubApplicationUser.Remove(clubApplicationUserToRemove);


... the deleted.Count() below returning 0 ... and the same with creating etc:



// Deleted
var deleted = ChangeTracker.Entries().Where(v => v.State == EntityState.Deleted &&
typeof(IBaseEntity).IsAssignableFrom(v.Entity.GetType())).ToList();

deleted.ForEach(entry =>
{
((IBaseEntity)entry.Entity).DateDeleted = DateTime.UtcNow;
((IBaseEntity)entry.Entity).DeletedBy = userId;
});


AssignClub.cshtml.cs -



using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
using AthlosifyWebArchery.Data;
using AthlosifyWebArchery.Models;
using AthlosifyWebArchery.Pages.Administrators.TournamentBatches;
using Microsoft.AspNetCore.Identity;
using System.ComponentModel.DataAnnotations;
using static AthlosifyWebArchery.Pages.Administrators.Users.AssignClubUserModel;
using Microsoft.AspNetCore.Http;
using System.Security.Claims;

namespace AthlosifyWebArchery.Pages.Administrators.Users
{
//public class AssignClubUserModel : ClubNamePageModel
public class AssignClubUserModel : UserViewPageModel
{
private readonly AthlosifyWebArchery.Data.ApplicationDbContext _context;
private readonly IHttpContextAccessor _httpContextAccessor;

public AssignClubUserModel(AthlosifyWebArchery.Data.ApplicationDbContext context,
IHttpContextAccessor httpContextAccessor
)
{
_context = context;
_httpContextAccessor = httpContextAccessor;
}

public class AssignClubUserViewModel<ApplicationUser>
{
public string Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string UserName { get; set; }
public Guid ClubID { get; set; }
public Guid OriginalClubID { get; set; }
public byte RowVersion { get; set; }
}

[BindProperty]
public AssignClubUserViewModel<ApplicationUser> AssignClubUser { get; set; }

//public SelectList ClubNameSL { get; set; }

public async Task<IActionResult> OnGetAsync(Guid? id)
{
if (id == null)
return NotFound();

AssignClubUser = await _context.Users
.Include(u => u.ClubApplicationUsers)
.Where(t => t.Id == id.ToString())
.Select(t => new AssignClubUserViewModel<ApplicationUser>
{
Id = t.Id,
FirstName = t.FirstName,
LastName = t.LastName,
UserName = t.UserName,
ClubID = t.ClubApplicationUsers.ElementAt(0).ClubID,
OriginalClubID = t.ClubApplicationUsers.ElementAt(0).ClubID,
RowVersion = t.ClubApplicationUsers.ElementAt(0).RowVersion
}).SingleAsync();

if (AssignClubUser == null)
return NotFound();


// Use strongly typed data rather than ViewData.
//ClubNameSL = new SelectList(_context.Club, "ClubID", "Name");

PopulateClubsDropDownList(_context, AssignClubUser.ClubID);

return Page();
}

public async Task<IActionResult> OnPostAsync(Guid id)
{
if (!ModelState.IsValid)
return Page();

// 1st approach:
// Modify the bridge model directly

/*var clubApplicationUserToUpdate = await _context.ClubApplicationUser
.FirstOrDefaultAsync(m => m.Id == id.ToString()
&& m.ClubID == AssignClubUser.OriginalClubID);

if (clubApplicationUserToUpdate == null)
return await HandleDeletedUser();

_context.Entry(clubApplicationUserToUpdate)
.Property("RowVersion").OriginalValue = AssignClubUser.RowVersion;

// This slightly tweek for this particular
// As the modified Change Track is not triggered

//_context.Entry(clubApplicationUserToUpdate)
// .Property("LastDateModified").CurrentValue = DateTime.UtcNow;

//_context.Entry(clubApplicationUserToUpdate)
// .Property("LastModifiedBy").CurrentValue = _httpContextAccessor.HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier);

if (await TryUpdateModelAsync<ClubApplicationUser>(
clubApplicationUserToUpdate,
"AssignClubUser",
s => s.Id, s => s.ClubID))
{
try
{
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
catch (DbUpdateConcurrencyException ex)
{
var exceptionEntry = ex.Entries.Single();
var clientValues = (ClubApplicationUser)exceptionEntry.Entity;
var databaseEntry = exceptionEntry.GetDatabaseValues();
if (databaseEntry == null)
{
ModelState.AddModelError(string.Empty, "Unable to save. " +
"The club application user was deleted by another user.");
return Page();
}

var dbValues = (ClubApplicationUser)databaseEntry.ToObject();
await setDbErrorMessage(dbValues, clientValues, _context);

AssignClubUser.RowVersion = (byte)dbValues.RowVersion;

ModelState.Remove("User.RowVersion");
}
}
*/

// 2nd approach:
// Soft -Delete and Add
// Did the soft-deleting and managed to add a new one BUT then die the roll back (adding the old one)
// Result: Violation of PRIMARY KEY constraint 'PK_ClubApplicationUser'.
// Cannot insert duplicate key in object
// Due to duplicate key

/*var clubApplicatonUserToRemove = await _context.ClubApplicationUser
.FirstOrDefaultAsync(m => m.Id == id.ToString());
ClubApplicationUser clubApplicatonUserToAdd = new ClubApplicationUser();
clubApplicatonUserToAdd.Id = id.ToString();
clubApplicatonUserToAdd.ClubID = AssignClubUser.SelectedClubID;


//_context.Entry(clubApplicatonUserToRemove)
// .Property("RowVersion").OriginalValue = User.RowVersion;

if (clubApplicatonUserToRemove != null)
{
_context.ClubApplicationUser.Remove(clubApplicatonUserToRemove);
await _context.SaveChangesAsync();clubApplicationUserDeleted
_context.ClubApplicationUser.Add(clubApplicatonUserToAdd);
await _context.SaveChangesAsync();
}*/

//delete all club memberships and add new one

//var clubApplicationUserDeleted = await _context.ClubApplicationUser
// .FirstOrDefaultAsync(m => m.Id == id.ToString()
// && m.ClubID == AssignClubUser.ClubID && m.IsDeleted == );

var userID = _httpContextAccessor.HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier);

if (AssignClubUser.ClubID != AssignClubUser.OriginalClubID)
{
var deletedClubApplicationUsers = _context.ClubApplicationUser.IgnoreQueryFilters()
.Where(post => post.Id == id.ToString()
&& post.ClubID == AssignClubUser.ClubID && EF.Property<bool>(post, "IsDeleted") == true);

if (deletedClubApplicationUsers.Count() > 0)
{
// Undo the deleted one
foreach (var deletedClubApplicationUser in deletedClubApplicationUsers)
{
var postEntry = _context.ChangeTracker.Entries<ClubApplicationUser>().First(entry => entry.Entity == deletedClubApplicationUser);
postEntry.Property("IsDeleted").CurrentValue = false;
postEntry.Property("LastDateModified").CurrentValue = DateTime.UtcNow;
postEntry.Property("LastModifiedBy").CurrentValue = userID;
postEntry.Property("DateDeleted").CurrentValue = null;
postEntry.Property("DeletedBy").CurrentValue = null;
}

// Removed the current one
var clubApplicationUserToRemove = await _context.ClubApplicationUser
.FirstOrDefaultAsync(m => m.Id == id.ToString()
&& m.ClubID == AssignClubUser.OriginalClubID);

clubApplicationUserToRemove.DateDeleted = DateTime.UtcNow;
clubApplicationUserToRemove.DeletedBy = userID;
_context.ClubApplicationUser.Remove(clubApplicationUserToRemove);

try
{
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
catch
{
PopulateClubsDropDownList(_context, AssignClubUser.ClubID);
return Page();
}
}
else
{
// Removed the current one
var clubApplicationUserToRemove = await _context.ClubApplicationUser
.FirstOrDefaultAsync(m => m.Id == id.ToString()
&& m.ClubID == AssignClubUser.OriginalClubID);
clubApplicationUserToRemove.DateDeleted = DateTime.UtcNow;
clubApplicationUserToRemove.DeletedBy = userID;
_context.ClubApplicationUser.Remove(clubApplicationUserToRemove);

try
{
_context.Entry(clubApplicationUserToRemove).State = EntityState.Deleted;
await _context.SaveChangesAsync();
}
catch
{
PopulateClubsDropDownList(_context, AssignClubUser.ClubID);
return Page();
}

// Added the new one
var newClubApplicationUser = new ClubApplicationUser()
{
Id = id.ToString(),
ClubID = AssignClubUser.ClubID,
DateCreated = DateTime.UtcNow,
CreatedBy = userID,
LastDateModified = DateTime.UtcNow,
LastModifiedBy = userID
};
_context.ClubApplicationUser.Add(newClubApplicationUser);

try
{
_context.Entry(newClubApplicationUser).State = EntityState.Added;
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
catch
{
PopulateClubsDropDownList(_context, AssignClubUser.ClubID);
return Page();
}
}
}

return RedirectToPage("./Index");
}

private async Task<IActionResult> HandleDeletedUser()
{
ClubApplicationUser deletedClubApplicationUser = new ClubApplicationUser();
ModelState.AddModelError(string.Empty,
"Unable to save. The club was deleted by another user.");
return Page();
}

private async Task setDbErrorMessage(ClubApplicationUser dbValues,
ClubApplicationUser clientValues, ApplicationDbContext context)
{
ModelState.AddModelError(string.Empty,
"The record you attempted to edit "
+ "was modified by another user after you. The "
+ "edit operation was canceled and the current values in the database "
+ "have been displayed. If you still want to edit this record, click "
+ "the Save button again.");
}

}
}


ClubApplicationUser.cs model:



using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Threading.Tasks;

namespace AthlosifyWebArchery.Models
{
public class ClubApplicationUser
{


public Guid ClubID { get; set; }

public Club Club { get; set; }

public string Id { get; set; }

public ApplicationUser ApplicationUser { get; set; }

public DateTime DateCreated { get; set; }

public string CreatedBy { get; set; }

public DateTime LastDateModified { get; set; }

public string LastModifiedBy { get; set; }

public DateTime? DateDeleted { get; set; }

public string DeletedBy { get; set; }

public bool IsDeleted { get; set; }

[Timestamp]
public byte RowVersion { get; set; }

[ForeignKey("CreatedBy")]
public ApplicationUser ClubApplicationCreatedUser { get; set; }

[ForeignKey("LastModifiedBy")]
public ApplicationUser ClubApplicationLastModifiedUser { get; set; }



}
}


Any ideas? The current solution is working OK BUT it just has extra routines to add these DateCreated, CreatedBy, LastDateModified, LastModifiedBy, DateDeleted and DeletedBy manually for this bridging model/class.



Environment:




  • .NET Core 2.2

  • SQL Server


Updates 1:



We even add this before the saving and it didn't trigger the automatic ChangeTracker.Entries():



_context.Entry(newClubApplicationUser).State = EntityState.Deleted;
await _context.SaveChangesAsync();


Updates 2:



Added the ClubApplicationUser model above.







asp.net-core .net-core razor-pages






share|improve this question















share|improve this question













share|improve this question




share|improve this question








edited Jan 18 at 21:46







dcpartners

















asked Jan 12 at 23:16









dcpartnersdcpartners

1,52783246




1,52783246













  • What is the defination for model? It would be helpful if you could share us a mini demo to reproduce your issue.

    – Tao Zhou
    Jan 14 at 5:35











  • @TaoZhou I've added the ClubApplicationUser model as per request.

    – dcpartners
    Jan 18 at 21:46



















  • What is the defination for model? It would be helpful if you could share us a mini demo to reproduce your issue.

    – Tao Zhou
    Jan 14 at 5:35











  • @TaoZhou I've added the ClubApplicationUser model as per request.

    – dcpartners
    Jan 18 at 21:46

















What is the defination for model? It would be helpful if you could share us a mini demo to reproduce your issue.

– Tao Zhou
Jan 14 at 5:35





What is the defination for model? It would be helpful if you could share us a mini demo to reproduce your issue.

– Tao Zhou
Jan 14 at 5:35













@TaoZhou I've added the ClubApplicationUser model as per request.

– dcpartners
Jan 18 at 21:46





@TaoZhou I've added the ClubApplicationUser model as per request.

– dcpartners
Jan 18 at 21:46












0






active

oldest

votes











Your Answer






StackExchange.ifUsing("editor", function () {
StackExchange.using("externalEditor", function () {
StackExchange.using("snippets", function () {
StackExchange.snippets.init();
});
});
}, "code-snippets");

StackExchange.ready(function() {
var channelOptions = {
tags: "".split(" "),
id: "1"
};
initTagRenderer("".split(" "), "".split(" "), channelOptions);

StackExchange.using("externalEditor", function() {
// Have to fire editor after snippets, if snippets enabled
if (StackExchange.settings.snippets.snippetsEnabled) {
StackExchange.using("snippets", function() {
createEditor();
});
}
else {
createEditor();
}
});

function createEditor() {
StackExchange.prepareEditor({
heartbeatType: 'answer',
autoActivateHeartbeat: false,
convertImagesToLinks: true,
noModals: true,
showLowRepImageUploadWarning: true,
reputationToPostImages: 10,
bindNavPrevention: true,
postfix: "",
imageUploader: {
brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
allowUrls: true
},
onDemand: true,
discardSelector: ".discard-answer"
,immediatelyShowMarkdownHelp:true
});


}
});














draft saved

draft discarded


















StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f54164758%2fdifferent-behaviour-between-bridging-and-standalone-model-class%23new-answer', 'question_page');
}
);

Post as a guest















Required, but never shown

























0






active

oldest

votes








0






active

oldest

votes









active

oldest

votes






active

oldest

votes
















draft saved

draft discarded




















































Thanks for contributing an answer to Stack Overflow!


  • Please be sure to answer the question. Provide details and share your research!

But avoid



  • Asking for help, clarification, or responding to other answers.

  • Making statements based on opinion; back them up with references or personal experience.


To learn more, see our tips on writing great answers.




draft saved


draft discarded














StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f54164758%2fdifferent-behaviour-between-bridging-and-standalone-model-class%23new-answer', 'question_page');
}
);

Post as a guest















Required, but never shown





















































Required, but never shown














Required, but never shown












Required, but never shown







Required, but never shown

































Required, but never shown














Required, but never shown












Required, but never shown







Required, but never shown







Popular posts from this blog

Liquibase includeAll doesn't find base path

How to use setInterval in EJS file?

Petrus Granier-Deferre