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.

Workflow Builder Rules

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.

​ Level Up Coding – Medium

about Infinite Loop Digital

We support businesses by identifying requirements and helping clients integrate AI seamlessly into their operations.

Gartner
Gartner Digital Workplace Summit Generative Al

GenAI sessions:

  • 4 Use Cases for Generative AI and ChatGPT in the Digital Workplace
  • How the Power of Generative AI Will Transform Knowledge Management
  • The Perils and Promises of Microsoft 365 Copilot
  • How to Be the Generative AI Champion Your CIO and Organization Need
  • How to Shift Organizational Culture Today to Embrace Generative AI Tomorrow
  • Mitigate the Risks of Generative AI by Enhancing Your Information Governance
  • Cultivate Essential Skills for Collaborating With Artificial Intelligence
  • Ask the Expert: Microsoft 365 Copilot
  • Generative AI Across Digital Workplace Markets
10 – 11 June 2024

London, U.K.