Search code examples
aspnetboilerplate

ASP.NET Boilerplate: What is the best solution for Four eyes principle


I want to add 4 eyes principle to ASP.NET Boilerplate framework. That means every change on Role, User,.. need to be approved (by another admin) before applied to the system. I have searched for some time but no answer. So what is the best solution for this flow?

Can I create the same tables with Abp tables (dbo.AbpUser_Temp, etc) and the all the changes will be stored in these tables? Is there any better solution?

Example: In the application, Admin1 has created a user named User1. But this user cannot login to the application until he was approved by Admin2.


Solution

  • Simple Workflows

    Example: In the application, Admin1 has created a user named User1. But this user cannot login to the application until he was approved by Admin2.

    Simple workflows like these can be appropriately handled by a property and a method:

    public class User : AbpUser<User>
    {
        public bool IsApproved { get; set; }
    
        public void Approve(User approver)
        {
            if (approver.Id != CreatorUserId)
            {
                IsApproved = true;
            }
        }
    }
    

    Complex Workflows

    Complex workflows like "every change" can do this instead of _Temp tables:

    public abstract class ChangeBase : Entity<long>, IExtendableObject
    {
        public string EntityTypeAssemblyQualifiedName { get; set; }
    
        public string EntityIdJsonString { get; set; }
    
        public long ProposerUserId { get; set; }
    
        public long? ApproverUserId { get; set; }
    
        public string ExtensionData { get; set; }
    }
    
    public class Change : ChangeBase
    {
        [NotMapped]
        public Type EntityType => Type.GetType(EntityTypeAssemblyQualifiedName);
    
        [NotMapped]
        public object EntityId => JsonConvert.DeserializeObject(EntityIdJsonString, EntityHelper.GetPrimaryKeyType(EntityType));
    
        [NotMapped]
        public bool IsApproved => ApproverUserId.HasValue && ApproverUserId != ProposerUserId;
    
        [NotMapped]
        public IDictionary<string, string> ChangedPropertyValuePairs => JObject.Parse(ExtensionData).ToObject<Dictionary<string, string>>();
    
        public Change(EntityIdentifier changedEntityIdentifier, long proposerUserId, IDictionary<string, string> changedPropertyValuePairs)
        {
            EntityTypeAssemblyQualifiedName = changedEntityIdentifier.Type.AssemblyQualifiedName;
            EntityIdJsonString = changedEntityIdentifier.Id.ToJsonString();
            ProposerUserId = proposerUserId;
            ExtensionData = JObject.FromObject(changedPropertyValuePairs).ToString(Formatting.None);
        }
    
        public bool Approve(long approverUserId)
        {
            if (approverUserId != ProposerUserId)
            {
                ApproverUserId = approverUserId;
                return true;
            }
    
            return false;
        }
    }
    

    Usage:

    public class UserAppService // ...
    {
        private readonly IRepository<Change, long> _changeRepository;
    
        public UserAppService(
            IRepository<User, long> repository,
            IRepository<Change, long> changeRepository) // : base(repository)
        {
            _changeRepository = changeRepository;
        }
    
        public void ChangeUserName(long userId, string newUserName)
        {
            // Validation, etc.
    
            var changedPropertyValuePairs = new Dictionary<string, string> {
                { nameof(User.UserName), newUserName }
            };
    
            var change = new Change(
                new EntityIdentifier(typeof(User), userId),
                AbpSession.GetUserId(),
                changedPropertyValuePairs
                );
    
            _changeRepository.Insert(change);
        }
    
        public void ApproveChange(long changeId)
        {
            // Validation, etc.
    
            var change = _changeRepository.Get(changeId);
    
            if (change.EntityType == typeof(User) && change.Approve(AbpSession.GetUserId()))
            {
                var user = Repository.Get((long)change.EntityId);
                var changedPropertyValuePairs = change.ChangedPropertyValuePairs;
    
                foreach (var changedProperty in changedPropertyValuePairs.Keys)
                {
                    switch (changedProperty)
                    {
                        case nameof(User.UserName):
                            user.UserName = changedPropertyValuePairs[changedProperty];
                            break;
                        // ...
                        default:
                            break;
                    }
                }
            }
        }