danielgerlag / danielgerlag/workflow-core

Unexpected behavior when implementing custom WorkflowCore.DSL.DefinitionLoader

Open
#1,092 1 comment 1 reaction 0 assignees View on GitHub
Dominant language
C#
Stars
5.9k
Forks
1.3k
Avg merge
1d 1h
Merged PRs (30d)
5

Description

**Describe the bug**
We are attempting to load assemblies of steps from a location other than appbin and execute them using WorkflowCore.DSL via json. to accomplish this we implemented a custom DefinitionLoader.

We started with the off the shelf DefinitionLoader and made the following modifications:

- Changed the "FindType" method on line 352 to the following:

```
private Type FindType(string name)
{
//Name passed in needs work
var nameArray = name.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries);
var nameToFind = nameArray[0].Split(new[] { "." }, StringSplitOptions.RemoveEmptyEntries)[1];

WorkflowCorePluginManager pluginManager = new WorkflowCorePluginManager();
return pluginManager.FindTypeByName("Steps", nameToFind);
}
```

- FindTypeByName method

```
Assembly pluginAssembly = LoadPlugin(pluginAssemblyName);
if (pluginAssembly != null)
{
//TypeInfo? typeInfo = pluginAssembly.DefinedTypes.FirstOrDefault(t => t.ImplementedInterfaces.Contains(typeof(IStepBody))
// && t.Name.Equals(name));
TypeInfo? typeInfo = pluginAssembly.DefinedTypes.FirstOrDefault(t => t.Name.Equals(name));
return typeInfo?.AsType();
}
```

- LoadPlugin method

```
private static Assembly LoadPlugin(string pluginAssemblyName)
{
// Navigate up to the solution root
string root = Path.Combine(Directory.GetCurrentDirectory(), "workflowcoreplugins");

string pluginLocation = Path.GetFullPath(Path.Combine(root, $"{pluginAssemblyName}.dll"));
Console.WriteLine($"Loading steps from assembly: {pluginLocation}");
PluginLoadContext loadContext = new PluginLoadContext(pluginLocation);
return loadContext.LoadFromAssemblyPath(pluginLocation);
}
```

- This is our step implmentation

```
using WorkflowCore.Interface;
using WorkflowCore.Models;

namespace Steps
{
public class TestStep : StepBody, IStepBody
{
public override ExecutionResult Run(IStepExecutionContext context)
{
Console.WriteLine("******************** This should work *************************");
return ExecutionResult.Next();
}
}
}
```

- This is the json we are passing to the loader

```
{
"Id": "NSRLSteps",
"Version": 1,
"Steps": [
{
"Id": "TestStep",
"StepType": "NSRLSteps.TestStep, NSRLSteps"
}
]
}
```

**To Reproduce**

1. Create a class library with a single step like below
```
using WorkflowCore.Interface;
using WorkflowCore.Models;

namespace NSRLSteps
{
public class TestStep : StepBody, IStepBody
{
public override ExecutionResult Run(IStepExecutionContext context)
{
Console.WriteLine("******************** This should work *************************");
return ExecutionResult.Next();
}
}
}
```

2. Create a console app that references version 3.7.0 WorkflowCore and WorkflowCore.DSL with a hosted service like below

```
using Elasticsearch.Net;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System;
using System.Threading;
using System.Threading.Tasks;
using WorkflowCore.Interface;
using WorkflowCore.Services.DefinitionStorage;

namespace Test
{
public class TestService : BackgroundService
{
private readonly IServiceProvider _serviceProvider;

public TestService(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}

protected async override Task ExecuteAsync(CancellationToken stoppingToken)
{
await Task.Run(() =>
{
var loader = _serviceProvider.GetRequiredService();
loader.LoadDefinition(@"{
""Id"": ""NSRLSteps"",
""Version"": 1,
""Steps"": [
{
""Id"": ""TestStep"",
""StepType"": "Steps.TestStep, Steps""
}
]
}
", Deserializers.Json);
});
}
}
}
```

- The issue occurs on line 72 of the DefinitionLoader `if (stepType.GetInterfaces().Contains(typeof(IStepBody)))`
- Our test step is not recognized as having implemented the IStepBody interface even though we have inherited from StepBody as instructed.
- We did find that updating the if to `if (stepType.GetInterfaces().Any(t => t.AssemblyQualifiedName == typeof(IStepBody).AssemblyQualifiedName))` does find our step. However we get the following exception on line 72:

```
System.ArgumentException
HResult=0x80070057
Message=GenericArguments[0], 'Steps.TestStep', on 'WorkflowCore.Models.WorkflowStep`1[TStepBody]' violates the constraint of type 'TStepBody'.
Source=System.Private.CoreLib
StackTrace:
at System.RuntimeType.ValidateGenericArguments(MemberInfo definition, RuntimeType[] genericArguments, Exception e)
at System.RuntimeType.MakeGenericType(Type[] instantiation)
at DefinitionLoader.ConvertSteps(ICollection`1 source, Type dataType) in DefinitionLoader.cs:line 72
at DefinitionLoader.Convert(DefinitionSourceV1 source) in DefinitionLoader.cs:line 38
at DefinitionLoader.LoadDefinition(String source, Func`2 deserializer) in CDefinitionLoader.cs:line 27
at TestService.b__2_0() in TestService.cs:line 29
at System.Threading.Tasks.Task.InnerInvoke()
at System.Threading.Tasks.Task.<>c.<.cctor>b__272_0(Object obj)
at System.Threading.ExecutionContext.RunFromThreadPoolDispatchLoop(Thread threadPoolThread, ExecutionContext executionContext, ContextCallback callback, Object state)

This exception was originally thrown at this call stack:
[External Code]

Inner Exception 1:
TypeLoadException: GenericArguments[0], 'Steps.TestStep', on 'WorkflowCore.Models.WorkflowStep`1[TStepBody]' violates the constraint of type parameter 'TStepBody'.
```

**Expected behavior**

TestStep should be successfully loaded.

**Additional context**

We would like to know if we are on the right track or if this is not a recommended course of action.

Thank you,
John

Contributor guide

No contributing guide indexed for this repository

Research direction

Start in DefinitionLoader.cs, especially ConvertSteps and line 72, and reproduce the failure through IDefinitionLoader.LoadDefinition with the provided JSON and custom assembly setup. Read WorkflowStep and the plugin-loading entry points to determine why the discovered TestStep fails the generic constraint; done means the external step assembly loads successfully and TestStep executes.

Written by the indexing model from the issue text.

Assessment

Tech stack
csharp
Domain
backend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.