workflow-core icon indicating copy to clipboard operation
workflow-core copied to clipboard

Unexpected behavior when implementing custom WorkflowCore.DSL.DefinitionLoader

Open jstimbertlhg opened this issue 2 years ago • 1 comments

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();
        }
    }
}
  1. 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<IDefinitionLoader>();
                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.<ExecuteAsync>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

jstimbertlhg avatar Sep 08 '22 19:09 jstimbertlhg

We had more or less the same problems. We solved it by adding a custom typeresolver like i proposed in #1166.

Then there is also some kind of assembly resolving etc. involved.

rapmue avatar May 04 '23 15:05 rapmue