Polymorphism: Workflow Builder
This part is the second of our four-part series on polymorphism. In the first part, we introduced polymorphism and discussed why it is important to understand this powerful feature of object-oriented programming. Now, we will delve deeper into implementing polymorphism through a walkthrough of our codebase, focusing on building a rule management system for our workflow builder.
A workflow consists of certain steps that execute when an action occurs in a particular module in the application. Let’s explore how polymorphism can be leveraged to build a flexible and maintainable workflow builder.

These rules can be applied across various modules in the application, each with its own specific business logic and rule implementations. By leveraging Object-Oriented Programming principles, we can implement this feature in a manner that is extendible and adheres to SOLID principles.
Defining the Base Class and Derived Classes
Folder structure:

Interfaces and Abstractions:
Using the IRule
Interface
In our workflow builder, we define an interface called IRule
to standardize the way rules are managed and executed across different modules. Using an interface helps us achieve several key benefits, especially in terms of extendibility and compliance with SOLID principles.
The IRule
Interface
Here’s the definition of the IRule
interface:
public interface IRule
{
string Id { get; set; }
string Context { get; set; }
bool IsRuleSupported(RuleKeyEnum keyEnum, ModuleMaster module);
Task<bool> EvaluateAsync(RuleHandlerContext context);
Task ExecuteAsync(RuleHandlerContext context);
}
Using BaseWhoRule Abstraction:
The BaseWhoRule
abstract class is designed to implement the IRule
interface, providing a foundational structure for creating specific rule implementations that handle permission validation. By leveraging this abstract base class, we can achieve a high degree of extensibility and maintainability in our codebase. Here's how BaseWhoRule
contributes to extensibility:
1. Code Reusability
The BaseWhoRule
class encapsulates common properties and methods that all rule implementations will share. By defining these shared elements in a base class, we avoid code duplication and ensure that common logic is centralized and reusable.
- Properties: The
Id
andContext
properties are implemented once in the base class, so derived classes do not need to re-implement them. - Methods: Utility methods like
CreateContext
and the default implementation ofEvaluateAsync
are also defined in the base class, allowing derived classes to use or override them as needed without rewriting the logic.
2. Ease of Maintenance
By centralizing shared functionality in BaseWhoRule
, maintaining the code becomes easier. Any changes to common behavior need to be made only in the base class, automatically propagating to all derived classes. This reduces the risk of errors and inconsistencies across different rule implementations.
3. Enforcement of Consistent Structure
The BaseWhoRule
class enforces a consistent structure for all rules. By inheriting from BaseWhoRule
, derived classes must implement the abstract methods (ExecuteAsync
and IsRuleSupported
). This ensures that every rule follows the same interface and provides the required functionalities.
4. Simplified Implementation of Derived Classes
Derived classes can focus on implementing specific logic without worrying about common infrastructure code. This simplifies the development of new rules, as the base class handles shared concerns.
public abstract class BaseWhoRule : IRule
{
public string Id { get; set; } = Guid.NewGuid().ToString();
private SystemCheckPermissionValidator _context;
public string Context
{
get => JsonConvert.SerializeObject(_context);
set
{
JsonSerializerSettings settings = new JsonSerializerSettings();
settings.Converters.Add(new BaseFilterListConverter());
_context = JsonConvert.DeserializeObject<SystemCheckPermissionValidator>(value, settings);
}
}
protected virtual SystemCheckPermissionValidatorContext CreateContext(RuleKeyEnum ruleKey, object parameters)
{
return new SystemCheckPermissionValidatorContext
{
Parameters = parameters != null ? JsonConvert.DeserializeObject<SystemCheckPermissionValidator>(parameters.ToString()) : new SystemCheckPermissionValidator()
};
}
public virtual async Task<bool> EvaluateAsync(RuleHandlerContext context)
{
var resource = context.Resource as SystemValidatorRuleContext;
if(resource == null)
return false;
var linkValidatorParameter = context.Link.Data.Who.Select(s=> JsonConvert.DeserializeObject<SystemCheckPermissionValidator>(s.Context.ToString(), new JsonSerializerSettings
{
NullValueHandling = NullValueHandling.Ignore
})).ToList();
foreach(var linkValidator in linkValidatorParameter)
{
if(linkValidator?.AllowedUsers == null && linkValidator?.AllowedRoles == null)
return true;
//check on document submitter id (-1) and userid of the document submitter
if(linkValidator?.AllowedUsers.Any(x => x.Id == GenericUserExtensions.DocumentSubmitterId
&& resource?.DocumentSubmitter == resource?.UserId) ?? false)
return true;
//check on document submitter id (-2) and userid of the document reporter
if(linkValidator?.AllowedUsers.Any(x => x.Id == GenericUserExtensions.DocumentReporterId
&& resource?.DocumentReported == resource?.UserId) ?? false)
return true;
//check on document submitter id (-3) and userid of the document reporter
if(linkValidator?.AllowedUsers.Any(x => x.Id == GenericUserExtensions.DocumentAssignee
&& resource?.DocumentAssignee == resource?.UserId) ?? false)
return true;
if(linkValidator?.AllowedUsers.Any(x => x.Id == resource?.UserId) ?? false)
return true;
if(linkValidator?.AllowedRoles.Any(x => resource?.UserRoles.Any(y => y.Id == x.Id) ?? false) ?? false)
return true;
if((linkValidator?.AllowedUsers.Any(x => x.Id == GenericUserExtensions.DocumentMembers) ?? false)
&& resource.memberIds != null && resource.memberIds.Any(x => x == resource.UserId))
return true;
}
return false;
}
public abstract Task ExecuteAsync(RuleHandlerContext context);
public abstract bool IsRuleSupported(RuleKeyEnum keyEnum, ModuleMaster module);
}
Using BaseRuleContext
for Extensibility
The BaseRuleContext
class provides a foundational structure for defining context objects used in rules. By using JSON polymorphism and defining derived types, BaseRuleContext
enhances extensibility and maintainability in our system. Here's a detailed explanation of how it contributes to extensibility and how it fits into the overall design.
The BaseRuleContext
Class
Here’s the definition of the BaseRuleContext
class:
[JsonPolymorphic]
[JsonDerivedType(typeof(SystemCheckPermissionValidator), typeDiscriminator: "systemPermissionValidator")]
[JsonDerivedType(typeof(SystemCheckUpdateFieldContext), typeDiscriminator: "systemUpdateFieldContext")]
[Serializable]
public class BaseRuleContext
{
public Guid Id { get; set; } = Guid.NewGuid();
public virtual RuleKeyEnum RuleKey { get; }
}
Benefits of Using BaseRuleContext
- Polymorphic Serialization
By using[JsonPolymorphic]
and[JsonDerivedType]
attributes,BaseRuleContext
supports polymorphic serialization. This means that derived types can be serialized and deserialized correctly, preserving their type information. This is crucial for scenarios where context objects need to be passed around in a distributed system or stored and retrieved from a database. - Extendibility with Derived Types
TheBaseRuleContext
class is designed to be extended by derived types. Each derived type can add specific properties and methods while inheriting the common structure fromBaseRuleContext
. This promotes extendibility, as new context types can be introduced without modifying existing code. - Consistency and Code Reuse
By centralizing common properties and logic inBaseRuleContext
, derived classes benefit from a consistent structure and shared functionality. This reduces code duplication and ensures that common behaviors are implemented uniformly across different context types. - Integration with Rule Implementations
The context objects defined byBaseRuleContext
and its derived types can be seamlessly integrated into rule implementations. For example, theBaseWhoRule
andBaseWhatRule
classes can use these context objects to perform their evaluations and actions.
[Serializable]
public class SystemCheckPermissionValidator : BaseRuleContext
{
public override RuleKeyEnum RuleKey => RuleKeyEnum.SystemPermissionValidator;
// Additional properties and methods specific to permission validation
}
[Serializable]
public class SystemCheckUpdateFieldContext : BaseRuleContext
{
public override RuleKeyEnum RuleKey => RuleKeyEnum.SystemUpdateFieldContext;
// Additional properties and methods specific to update field context
}
Implementations
We would now extend base abstraction in the implementation in the various modules EWO, Kaizen and Best Practice.
public class KaizenSystemCheckPermissionsValidator : BaseWhoRule
{
public override bool IsRuleSupported(RuleKeyEnum ruleKeyEnum, ModuleMaster module)
{
return ruleKeyEnum == RuleKeyEnum.SystemPermissionValidator && module == ModuleMaster.Kaizen;
}
public override async Task ExecuteAsync(RuleHandlerContext context)
{
//logic to execute business logic for the rule
}
}
public class EWOSystemCheckPermissionsValidator: BaseWhoRule
{
public override bool IsRuleSupported(RuleKeyEnum ruleKeyEnum, ModuleMaster module)
{
return ruleKeyEnum == RuleKeyEnum.SystemPermissionValidator && module.Value == ModuleMaster.EWO;
}
public override async Task ExecuteAsync(RuleHandlerContext context)
{
//logic to execute business logic for the rule
}
}
public class BestPracticeSystemCheckPermissionsValidator: BaseWhoRule
{
public override bool IsRuleSupported(RuleKeyEnum ruleKeyEnum, ModuleMaster module)
{
return ruleKeyEnum == RuleKeyEnum.SystemPermissionValidator && module.Value == ModuleMaster.BestPractice;
}
public override async Task ExecuteAsync(RuleHandlerContext context)
{
//logic to execute business logic for the rule
}
}
Invocation
A collection of rules that implement the IRule
interface. This allows for polymorphic behaviour where different rule implementations can be evaluated dynamically.
public class KaizenWorkflowApiController : BaseApiController
{
readonly IGenericRepository _genericRepository;
readonly IEnumerable<IRule> _rules;
readonly IEventPublisher _eventPublisher;
readonly IKaizenDomainService _kaizenDomainService;
readonly ILogger<KaizenWorkflowApiController> _logger;
public KaizenWorkflowApiController(IGenericRepository genericRepository, IEnumerable<IRule> rules,
IEventPublisher eventPublisher, IKaizenDomainService kaizenDomainService, ILogger<KaizenWorkflowApiController> logger)
{
_genericRepository = genericRepository;
_rules = rules;
_eventPublisher = eventPublisher;
_kaizenDomainService = kaizenDomainService;
_logger = logger;
}
[HttpPut("{id}/allowed-transitions/source/{source}/target/{target}")]
public async Task<IActionResult> UpdateTransition(Guid id, string source, string target,
DateTime? documentCloseDate = null, [FromQuery] string module = "Kaizen")
{
var rule = _rules.FirstOrDefault(x =>
x.IsRuleSupported(RuleKeyEnum.SystemPermissionValidator,
ModuleMaster.FromName(module))); //get module from front-end.
bool res = true;
var ruleHandlerContext = new RuleHandlerContext
{
Resource = id,
};
if (rule != null)
res = await rule.EvaluateAsync(ruleHandlerContext);
return Ok();
}
}
Startup.cs
#region Workflow
builder.Services.AddTransient<IRule, KaizenSystemCheckPermissionsValidator>();
builder.Services.AddTransient<IRule, KaizenSystemCheckUpdateField>();
builder.Services.AddTransient<IRule, KaizenSystemCheckFieldValue>();
builder.Services.AddTransient<IRule, KaizenSystemCheckNotification>();
builder.Services.AddTransient<IRule, EWOSystemCheckPermissionsValidator>();
builder.Services.AddTransient<IRule, BestPracticeSystemCheckPermissionsValidator>();
#endregion
Conclusion
By applying these principles and leveraging polymorphism, we created a flexible, maintainable, and scalable rule management system for our workflow builder. This approach not only ensures that the system can adapt to changing business requirements but also promotes clean, reusable, and testable code. As you continue to build and refine your applications, keep these principles in mind to achieve a robust and adaptable software architecture.
Stay tuned for the next parts of this series, where we will delve deeper into more advanced applications and benefits of polymorphism in software design.