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