This content originally appeared on Level Up Coding – Medium and was authored by Prabhpahul Singh
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 and Context 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 of EvaluateAsync 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
The BaseRuleContext class is designed to be extended by derived types. Each derived type can add specific properties and methods while inheriting the common structure from BaseRuleContext. This promotes extendibility, as new context types can be introduced without modifying existing code. - Consistency and Code Reuse
By centralizing common properties and logic in BaseRuleContext, 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 by BaseRuleContext and its derived types can be seamlessly integrated into rule implementations. For example, the BaseWhoRule and BaseWhatRule 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.
Polymorphism: Workflow Builder was originally published in Level Up Coding on Medium, where people are continuing the conversation by highlighting and responding to this story.
This content originally appeared on Level Up Coding – Medium and was authored by Prabhpahul Singh