I am building a .Net Core Blazor Server application. The application creates records of a Project
class which move through a workflow with various phases of review and acceptance/rejection before completion.
I am trying to determine the best place to incorporate various business rules around who can modify the Project
's state and when. Specifically, I'm trying to determine if it's ok to add current user data and claims principal data into an object's state.
As a simplified example, let's say the Project
has three states: DraftState
, ReviewState
, and CompleteState
. In order to determine who can move this project through the workflow, we consider three pieces of information:
- The current state of the project (where it is in the workflow)
- If the current user's role, either
NormalUser
,ProjectReviewer
, orAdmin
- If the current user is listed as the project's
Designer
, meaningProject.DesignerId == CurrentUser.Id
The desired rules are as follows:
- From
DraftState
, the project's designer or any user in theAdmin
role can move it toReviewState
- From
ReviewState
, any user in theProjectReviewer
or in theAdmin
role can move it toCompleteState
or return it toDraftState
- From the
CompleteState
, the project can't be modified further.
I built my Project
class to contain a standard state pattern to control the object's behavior based on state, but I'm not sure what the best practice is for incorporating the user and roles based rules to this system.
As I can tell, I have three options
- Build the UI in Blazor the limit access to the
Project
's state methods by looking at the user's data and showing/hiding UI controls. This requires all sorts of If/else statements and nests. Not extensible. - Pass the
CurrentUser
data and theClaimsPrincipal.IsInRole()
results into the state of theproject
, so that theproject
's state can accurately analyze ALL of the required business rules and change behavior. Superficially, this strikes me as the most straightforward solution, BUT, but I worry about injecting UI and Roles-based information from Blazor into my data classes. I don't know if this is appropriate - Wrap the
Project
state pattern into a larger state pattern that controls workflow behavior. This is an additional level of abstraction, but it lets me incorporate the three data sources (CUrrentUser
,CurrentUserRoles
, andIsInRole("Admin")
in separate groups wit their own concerns. I have no code sample for this, but one would be much appreciated.
My goals for all of this is to:
- Accurately follow an object through the workflow
- Simplify long-term management
- Allow for future modification and additional steps in the workflow
My apologies if this belong in Code Review or some other network. I'm happy to move it.
1 Answer 1
When the only thing which changes with the Project's state is the list of permitted states which are allowed to come next (under certain conditions), I am not sure if your current approach of using a state pattern is really justified. At a first glance, it seems to add unncessary complexity. Of course, I may not know enough about your case in full. Still I would avoid distributing the business rules over several child classes of a base class State
, since changes to those rules would typically cause changes to all childs and the base class, which is not really desirable.
The most simple solution I can think of is this:
I would make a separate service class
NextStatesService
which gets the Project's state, the current user's role, their relationship to the project and whatever else is required for evaluating the business rules.The service then provides a function which returns the list of available "next states" under the current conditions. Of course, if you need the information in different forms for controlling the UI and/or the workflow, feel free to add such functions to the service as well.
The advantage is that only this new service will depend on your data classes and UI/role-based classes, so this doesn't lead to a dependency of the Project on these classes (not even one which needs to be injected at run time). I don't know if these dependencies would really cause some trouble in your case, that is something you can find out by writing some tests for the Project
class and check if the test setup starts to make trouble when you try to move the state logic into the Project class itself.
How you implement the service internally is something I would judge when seeing the complexity of the code and the actual rules beyond your (probably simplified) example. When a few conditionals are sufficient, one can keep those. When the code appears to become too complex, one could switch to a decision-table based approached, or delegate parts of the logic to the Project
class, or other classes like User
and Role
, assumed those are under your control and not part of the framework.
When you decide for keeping the State
pattern, parts of the logic might also be delegated to the different State
subclasses, ideally things which are mostly stable in this model. But as I wrote, I would not start with this design, but try a simpler approach first and only refactor to more classes when the codebase grows and the overhead for these classes is clearly justified.
TLDR: delegate the problem to a third instance which adds another level of indirection. Think big, but start small. Use the most simply solution you can think of which does not introduce unwanted dependencies, and refactor to more classes only when needed.
-
Thanks much for you input. I refactored my code, and I do think that it's much easier to follow. The service did end up with a lot of very, very similar (but not identical) switch statements that map out what
ProjectStatus
can go to what other status depending on user and roles. Although at first glance it appears repetitive, having it all in one class allows you to compare all of them more readily rather than navigating through the State pattern classes. My state pattern approach did make each individual state more clear-cut and extensible, but it hid the big picture of the workflow rules.aterbo– aterbo2023年03月01日 05:01:38 +00:00Commented Mar 1, 2023 at 5:01
Explore related questions
See similar questions with these tags.