Suppose I have a command like below:
public sealed class UpdateExampleCommand
{
public int Field1 { get; set; }
public string Field2 { get; set; }
public bool Field3 { get; set; }
public double Field4 { get; set; }
public DateTime Field5 { get; set; }
}
There is a permission that determines whether the user can perform the UpdateExampleCommand
. This form of authorization is very simple and can be treated as a cross-cutting concern by checking the user's permissions in a decorator before rejecting or accepting the command.
However, there are also permissions that determine whether a user can edit particular fields that's modified by the UpdateExampleCommand
.
e.g. A user requires the Can_Edit_Field_3
permission to make changes to Field3
's value, Can_Edit_Field_4
permission to make changes to Field4
, and Can_Edit_Field_5
permission to make changes to Field5
.
Solution 1
Would this qualify as business logic and belong in the aggregate root like below?
public void UpdateExample(int field1, string field2, bool field3, double field4, DateTime field5, Editor editor)
{
// Protect invariants
// ...
// Perform updates
Field1 = field1;
Field2 = field2;
// Editor is a value object encapsulating the user permissions.
// It is created in the command handler based on the user context
// and passed into the aggregate root's method.
//
// There are several other permissions like this that aren't strictly
// bound to roles, so I can't use an approach like the "CollaboratorService"
// from the IDDD book which performs an implicit permission check
// by creating a Moderator, Author, etc. role object if the current
// user has that role.
if (editor.CanEditField3) Field3 = field3;
if (editor.CanEditField4) Field4 = field4;
if (editor.CanEditField5) Field5 = field5;
}
Solution 2
Or would it be preferable to create separate methods in the aggregate root and check permissions in the command handler before calling the appropriate method?
public async Task<Unit> Handle(UpdateExampleCommand request, CancellationToken cancellationToken)
{
// Load aggregate
// ...
entity.UpdateExample(request.Field1, request.Field2);
if (user.HasPermission("Can_Edit_Field_3"))
{
entity.UpdateField3(request.Field3);
}
if (user.HasPermission("Can_Edit_Field_4"))
{
entity.UpdateField4(request.Field4);
}
if (user.HasPermission("Can_Edit_Field_5"))
{
entity.UpdateField5(request.Field5);
}
// Save aggregate
// ...
}
Solution 3
Or should I create multiple different commands each with their own authorization decorators?
e.g. UpdateExampleCommand
, UpdateField3Command
, UpdateField4Command
, UpdateField5Command
This would extract all authorization concerns from the handlers and models, so the code would be cleaner and easier to reason with, but it would come at a performance cost because the aggregate would be loaded multiple times for a single process (it might be loaded 8 times in this example if the authorization decorators need to check the state of the aggregate as well), and the responsibility to issue the right commands would be shifted to the application client.
Conclusion
Ultimately, I'm not sure what should influence the business processes modeled by the domain.
The command was initially made into a single command because that's how the UI is laid out and it's how the business works in general. However, allowing the UI to influence the domain model causes authorization logic to leak into the domain model.
Allowing authorization concerns to influence the domain model (so that the single command is split into multiple) allows the code to be cleaner, but it may make the business process less expressive, as the application client is charged with calling the commands in the correct order.
Is there a general rule of thumb for these situations or is there another way to do this that I'm missing?