Base solution for your next web application
Open Closed

Entity History shows duplicate (and faulty) audited properties for User entities #11761


User avatar
1
dirkvr created

We recently enabled Entity History in our project. For most entities this seems to work properly aas intended. For (our subclassed entity of) AbpUser this logs duplicates of the audited properties (CreatorUserId, LastModifierUserId, ...) with faulty 'OriginalValue' data.

Row 3 shows the correct update from Id 663 to Id 13493 for the LastModifierUser. Rows 5-9 show faulty entries for the LastModifierUserId, where for some reason the Original Value is a string instead of a long, and does not correspond to the actual original value.

In order to debug this, I tried to subclass the EntityHistoryHelper and override CreateEntityChangeSet, and was able to verify that the returned changeset is correct. So these properties are added later in the flow, either somewhere between the call to CreateEntityChangeSet and the actual save to the db table, or during the actual save.


8 Answer(s)
  • User Avatar
    0
    ismcagdas created
    Support Team

    Hi @dirkvr

    Is it possible to share your User class and the code which creates or modifies the user entity ?

    Thanks,

  • User Avatar
    0
    dirkvr created
    [Audited]
    [AuditExpansion(nameof(TenantId), typeof(Tenant), nameof(RmoniWeb.MultiTenancy.Tenant.Name))]
    public class User : AbpUser<User>
    {
        public virtual Guid? ProfilePictureId { get; set; }
    
        public virtual bool ShouldChangePasswordOnNextLogin { get; set; }
    
        public DateTime? SignInTokenExpireTimeUtc { get; set; }
    
        public string SignInToken { get; set; }
    
        public string GoogleAuthenticatorKey { get; set; }
    
        public List<ExtendedUserOrganizationUnit> OrganizationUnits { get; set; }
        public ICollection<ReportUserRecipient> ReportUserRecipients { get; set; }
        public ICollection<AlarmUserRecipient> AlarmUserRecipients { get; set; }
        public ICollection<Device> MobileDevices { get; set; }
        public bool IsRecipient { get; set; }
        public Tenant Tenant { get; set; }
        [ForeignKey("ApplicationLanguage")]
        public int? LanguageId { get; set; }
        public ApplicationLanguage ApplicationLanguage { get; set; }
        //Can add application specific user properties here
    
        public User()
        {
            IsLockoutEnabled = true;
            IsTwoFactorEnabled = true;
        }
    
        public static User CreateTenantAdminUser(int tenantId, string emailAddress, string phoneNumber = default)
        {
            var user = new User
            {
                TenantId = tenantId,
                UserName = AdminUserName,
                Name = AdminUserName,
                Surname = AdminUserName,
                EmailAddress = emailAddress,
                PhoneNumber = phoneNumber,
                Roles = new List<UserRole>(),
                OrganizationUnits = new List<ExtendedUserOrganizationUnit>()
            };
    
            user.SetNormalizedNames();
    
            return user;
        }
    
        public override void SetNewPasswordResetCode()
        {
            PasswordResetCode = Guid.NewGuid().ToString("N").Truncate(10).ToUpperInvariant();
        }
    
        public void Unlock()
        {
            AccessFailedCount = 0;
            LockoutEndDateUtc = null;
        }
    
        public void SetSignInToken()
        {
            SignInToken = Guid.NewGuid().ToString();
            SignInTokenExpireTimeUtc = Clock.Now.AddMinutes(1).ToUniversalTime();
        }
    }
    
  • User Avatar
    0
    dirkvr created
    [AbpAuthorize(AppPermissions.Pages_Administration_Users_Edit)]
    protected virtual async Task UpdateUserAsync(CreateOrUpdateUserInput input)
    {
        var user = await UserManager.FindByIdAsync(input.User.Id.Value.ToString());
    
        //Update user properties
        ObjectMapper.Map(input.User, user); //Passwords is not mapped (see mapping configuration)
    
        CheckErrors(await UserManager.UpdateAsync(user));
    
        if (input.SetRandomPassword)
        {
            var randomPassword = await _userManager.CreateRandomPassword();
            user.Password = _passwordHasher.HashPassword(user, randomPassword);
            input.User.Password = randomPassword;
        }
        else if (!input.User.Password.IsNullOrEmpty())
        {
            await UserManager.InitializeOptionsAsync(AbpSession.TenantId);
            CheckErrors(await UserManager.ChangePasswordAsync(user, input.User.Password));
        }
    
        //Update roles
        CheckErrors(await UserManager.SetRolesAsync(user, input.AssignedRoleNames));
    
        //update organization units
        await UserManager.SetOrganizationUnitsAsync(user, input.OrganizationUnits.ToArray());
    
        if (AbpSession.TenantId.HasValue)
        {
            await _cacheManager.GetUserOrganizationUnitIdsRecursiveCache(AbpSession.GetTenantId()).ClearAsync();
            await _cacheManager.GetUserOrganizationUnitIdsCache(AbpSession.GetTenantId()).ClearAsync();
            await _cacheManager.GetUserOrganizationUnitsRecursiveCache(AbpSession.GetTenantId()).ClearAsync();
        }
    
        if (input.SendActivationEmail)
        {
            user.SetNewEmailConfirmationCode();
            await _userEmailer.SendEmailActivationLinkAsync(
                user,
                AppUrlService.CreateEmailActivationUrlFormat(AbpSession.TenantId),
                input.User.Password
            );
        }
    }
    

    UserManager custom implementations:

    public override Task<IdentityResult> SetRolesAsync(User user, string[] roleNames)
    {
        if (user.Name == "admin" && !roleNames.Contains(StaticRoleNames.Host.Admin))
        {
            throw new UserFriendlyException(L("AdminRoleCannotRemoveFromAdminUser"));
        }
    
        return base.SetRolesAsync(user, roleNames);
    }
    
  • User Avatar
    0
    ismcagdas created
    Support Team

    Hi,

    Thanks. What does AuditExpansion attribute do ?

  • User Avatar
    0
    dirkvr created

    The AuditExpansion attribute is used to provide extra context for certain properties in the UI. On Application start-up, a dictionary is built based on these attributes, so the endpoint that fetches the data knows where to fetch context. So if an entity has a UserId with a value of 5 that would be displayed as 5 (UserName)

    That being said, The issue is happening without the attribute as well, and the duplicate properties are inserted in the DB on save, before we even attempt to fetch it for the front-end. at this point, no action has been taken yet due to the attribute

  • User Avatar
    0
    ismcagdas created
    Support Team

    Hi @dirkvr

    In that case, is it possible to share your project via email ([email protected]) so we can reproduce this problem ?

    Thanks,

  • User Avatar
    0
    dirkvr created

    The project should be in your inbox, Please let us know if you require additional information

  • User Avatar
    0
    ismcagdas created
    Support Team

    Hi @dirkvr

    Thanks, we got the project and will reply back via email.