Различное поведение между мостом и автономной моделью / классом - PullRequest
0 голосов
/ 13 января 2019

Мы пытаемся упростить наши поля автоматического обновления для DateCreated, CreatedBy, LastDateModified, LastModifiedBy, DateDeleted и DeletedBy и работали нормально, добавив подпрограмму OnBeforeSaving в ApplicationDBContext.cs, и мы также делаем SOFT-DELETE для сохранения записей (помеченных как IsDeleted подход), когда мы удалили:

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
            }
        }
    }
}

Работал нормально с автономной моделью / классом т.е. Club, Host, ApplicationUser ** НО не соединяющая модель / класс т.е. ** ClubApplicationUser и HostApplicationUser

Таким образом, мы должны сделать это вручную (например, AssignClub.cshtml.cs), добавив вручную для создания / удаления, как показано ниже:

// 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);

... удалено. Count () ниже возвращает 0 ... и то же самое с созданием и т. Д .:

// 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:

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; }



    }
}

Есть идеи? Текущее решение работает нормально, НО у него просто есть дополнительные подпрограммы для добавления этих DateCreated, CreatedBy, LastDateModified, LastModifiedBy, DateDeleted и DeletedBy вручную для этой модели / класса моста.

Окружающая среда:

  • .NET Core 2.2
  • SQL Server

Обновления 1:

Мы даже добавили это перед сохранением, и оно не включило автоматическое ChangeTracker.Entries () :

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

Обновления 2:

Добавлена ​​модель ClubApplicationUser выше.

...