OrchardCMS / OrchardCore

Orchard Core is an open-source modular and multi-tenant application framework built with ASP.NET Core, and a content management system (CMS) built on top of that framework.
https://orchardcore.net
BSD 3-Clause "New" or "Revised" License
7.36k stars 2.37k forks source link

Implementation notes for Workflow wireup #3902

Closed AmrTealeb closed 4 months ago

AmrTealeb commented 5 years ago

We are trying to wire up the workflows in our project First i want to choose a workflow type, instantiate it and pass some values to it So i created this workflow using the designer Design

I noticed the WorkflowManager class so i decided to use it

Then i write some code to:

  1. Get workflow type by id
  2. Crete new workflow
  3. Create workflow execution context
  4. Run activities one by one

`

        var input = new Dictionary<string, object>();

        input.Add("Title", Input.Title);

        input.Add("Content", Input.Content);

        var workflowType = await _workflowTypeStroe.GetAsync(WORKFLOW_ID);

        var workflow = _workflowManager.NewWorkflow(workflowType);

        var context = await _workflowManager.CreateWorkflowExecutionContextAsync(workflowType, workflow, input);

        var activities = workflowType.Activities;
        var remainingActivities = workflowType.Activities.AsEnumerable();

        while (remainingActivities.Any())
        {
            remainingActivities = await _workflowManager.ExecuteWorkflowAsync(context, remainingActivities.First());
        }

`

but then workflow.Status is Faulted and workflow.FaultMessage is "Error Specified argument was out of the range of valid values. Parameter name: column Actual value was 1."

sebastienros commented 5 years ago

Can you share the full stack trace of the exception?

/cc @sfmskywalker

sfmskywalker commented 5 years ago

There probably won’t be a stack trace if the workflow manager catches and swallows any exceptions, which I believe it does. Instead of rethrowing it sets the workflow’s status to Faulted.

@amrtealeb is there any chance you could share your source code with me? If not I’ll try and reproduce over the weekend and see what’s going on.

sfmskywalker commented 5 years ago

Actually your code above should suffice.

sfmskywalker commented 5 years ago

I do wonder what the type of Input.Content is, as well as its value?

sfmskywalker commented 5 years ago

I looked into it, and I have a couple of observations and questions.

I tried setting up the exact same workflow as depicted above, however I failed to find the activity displayed as "Validate Content" where I could set a minimum and maximum. Is this a custom activity?

I continued setting up the workflow without the "Validate Content" activity and copied your code into a controller, then executed that controller. The workflow completed successfully with no faults. Therefore I assume there's something not right with the "Validate Content" activity.

Anyway, I'm curious as to why you'd be manually invoking a workflow that starts with the HttpRequestEvent. Such workflows will already be invoked for you. And better yet - each and every blocking activity will have an appropriate support class that will cause halted workflows to be resumed.

Your approach of automatically resuming halted workflows immediately will defeat the purpose of having workflows that can be edited by a dashboard user, because now your code will have to understand each type of blocking activity of when it should be resumed and with what stimuli.

That being said, if you have short-lived workflows (workflows that don't contain any blocking activities), you can take the approach you depicted above. All you'd need to do then is remove the part where you check for remaining (blocking) activities, since there won't be any. You can then also just remove the HttpRequestEvent as the starting activity, and instead mark the "Set Article" activity as the start activity. Your workflow will now behave as a regular function that receives input and returns output (accessible via the returned workflow execution context).

But if you do want to manually invoke a specific workflow that is potentially long-running, I recommend that all you do is start the workflow (using _workflowManager.StartWorkflowAsync), and not automatically resume the workflow, as it won't make sense. Nor would you have to do so: as soon as there's a halted workflow instance, it will be resumed when the appropriate event & stimuli is received.

I hope this helps, but let me know if you have any questions or doubts.

dodyg commented 5 years ago

Is it possible to have the workflow defined in code? Because it seems to me at the moment that it can only be configured via the designer which serialize the workflow in json.

AmrTealeb commented 5 years ago

Thank you for your reply Validate Content is a custom activity I tried using _workflowManager.StartWorkflowAsync for a HttpRequestEvent and it works fine but i have some questions here When i run await _workflowManager.StartWorkflowAsync(workflowType, startActivity); inside a controller method how does my payload get passed to the StartWorkflowAsyync method?

sfmskywalker commented 5 years ago

@dodyg You can certainly define a workflow in code. You'd instantiate a new WorkflowType, set its Activities and Transitions properties, and persist it. For example, the following code defines a workflow type consisting of an HTTP Request Event, an HTTP Response Action, a transition connecting the two, and persisting it so it can be instantiated:

var activity1 = new ActivityRecord
{
    ActivityId = Guid.NewGuid().ToString("N"),
    Name = nameof(HttpRequestEvent),
    IsStart = true,
    Properties = JObject.FromObject(new
    {
        Url = "http://foo",
        Method = "GET"
    })
};

var activity2 = new ActivityRecord
{
    ActivityId = Guid.NewGuid().ToString("N"),
    Name = nameof(HttpResponseTask),
    Properties = JObject.FromObject(new
    {
        HttpStatusCode = 200,
        Content = new WorkflowExpression<string>("Hello World!")
    })
};

var transition = new Transition
{
    Id = 1,
    SourceActivityId = activity1.ActivityId,
    DestinationActivityId = activity2.ActivityId,
    SourceOutcomeName = "Done"
};

var workflowType = new WorkflowType
{
    Activities = new [] { activity1, activity2 },
    Transitions = new [] { transition }
};

await _workflowTypeStore.SaveAsync(workflowType);
sfmskywalker commented 5 years ago

@AmrTealeb

There's a 3rd argument that takes a dictionary representing your payload (called input). Here's the full method signature:

Task<WorkflowExecutionContext> StartWorkflowAsync(WorkflowType workflowType, ActivityRecord startActivity = null, IDictionary<string, object> input = null, string correlationId = null)

dodyg commented 5 years ago

@sfmskywalker OK great. Let me try this out and add some helper functions to type the properties creation. JObject.FromObject is super flexible but prone to typos.

sfmskywalker commented 5 years ago

@dodyg I gave your question some more thought, and realized that I might have missed an important aspect to your question: would it be possible to define a workflow in code and have it participate in the workflow triggering system, without having to persist it? The short answer to that would be 'no', because your workflow defined in code wouldn't be available to the workflow manager otherwise.

However I've been researching other open source workflow libraries, and came across a project called Workflow Core, which enables one to define a workflow in code and have it executed. Unfortunately, it doesn't have a designer, so you would be stuck to defining all of your workflows in code (which might be sufficient in many scenarios, not certainly not all).

I've been wanting for a long time now to take the best of some of the worlds out there:

  1. Orchard Core Workflows with its neat web-based designer
  2. The ability to define & execute workflows in code
  3. The ability to store workflows as part of source control
  4. The ability to use workflows outside the context of Orchard
  5. The ability to rehost the workflow designer in my own app and have it a pure client-side HTML5 web component.

To achieve all that, I'm working on Elsa Workflows that basically consists of two projects: a standalone HTML5 web based designer, and a .NET Standard workflow library that allows you to define workflows in code like Workflow Core and load & execute workflow definitions & instances defined as YAML.

I'm almost at the point where I can start working on a gallery module for Orchard as a proof of concept. I don't know if there would be any interest in taking it in, or if it would be even feasible, but I want to try.

dodyg commented 5 years ago

I am investigating of the suitability of using the Orchard Core workflow in a business application. If the persistence is a requirement, it should not a deal breaker as long as we find a way to override the persistence mechanism to other storage other than YesSql.

I think one of the main appeal of a workflow system is its accessibility to expert users. Having a workflow without a web designer really limits its applicability.

I discovered Esla Workflow yesterday and filed a question(https://github.com/elsa-workflows/elsa-core/issues/47). I think yeah integrating Esla to be part of Orchard Core ecosystem is going to be important because the Orchard Core ecosystem will be big once it reaches some level of maturity. There is no other such ecosystem exists in .NET Core world.

Kinani commented 5 years ago

If I'm going to include Workflows in a business application (not CMS). Should I go with Elsa-Workflows or OrchardCore.Workflows?

AmrTealeb commented 5 years ago

I did a new implementation for WorkflowTypeStore to save data in SQL server, how can i register my new class to be used for IWorkflowTypeStore interface? I tried this in my Startupcs file but didn't work services.AddScoped<IWorkflowTypeStore, WorkflowTypeSqlStore>();

jtkech commented 5 years ago

Try to add in your module manifest a dependency on the workflow feature so that your startup will run after the workflow one, this way i think you will be able to really override the default implementation.

dodyg commented 5 years ago

If I'm going to include Workflows in a business application (not CMS). Should I go with Elsa-Workflows or OrchardCore.Workflows?

You can use Orchard Core Framework for business application just fine. @AmrTealeb is trying out OrchardCore.Workflows in business application. I am investigating esla-workflow. I suspect you can go either way but trying it out in a test project is the only way to find out.

AmrTealeb commented 5 years ago

@jtkech Thank you, i did this and it works. Is there a way i make the CMS use my implementation for IWorkflowTypeStore and IWorkflowStore ?

sfmskywalker commented 5 years ago

@Kinani I recommend going with OrchardCore.Workflows for now, as Elsa is not ready yet for prime time. After that however, things might be different.

Kinani commented 5 years ago

@Kinani I recommend going with OrchardCore.Workflows for now, as Elsa is not ready yet for prime time. After that however, things might be different.

Okay, thank you all for your great contributions.

Kinani commented 5 years ago

I tried to include OrchardCore.Workflows as advised, so far what i did:

I get the following Exception

Unable to resolve service for type `YesSql.ISession' while attempting to activate 'OrchardCore.Workflows.Http.Services.HttpRequestRouteActivator'.

Stack trace: at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateArgumentCallSites(Type serviceType, Type implementationType, CallSiteChain callSiteChain, ParameterInfo[] parameters, Boolean throwIfCallSiteNotFound) at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateConstructorCallSite(Type serviceType, Type implementationType, CallSiteChain callSiteChain) at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.TryCreateExact(ServiceDescriptor descriptor, Type serviceType, CallSiteChain callSiteChain) at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.TryCreateEnumerable(Type serviceType, CallSiteChain callSiteChain) at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateCallSite(Type serviceType, CallSiteChain callSiteChain) at Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngine.CreateServiceAccessor(Type serviceType) at System.Collections.Concurrent.ConcurrentDictionary2.GetOrAdd(TKey key, Func2 valueFactory) at Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngine.GetService(Type serviceType, ServiceProviderEngineScope serviceProviderEngineScope) at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(IServiceProvider provider, Type serviceType) at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService[T](IServiceProvider provider) at OrchardCore.Modules.ModularTenantContainerMiddleware.Invoke(HttpContext httpContext) in C:\Users\Kinani\Source\OrchardCore\src\OrchardCore\OrchardCore\Modules\ModularTenantContainerMiddleware.cs:line 76

sebastienros commented 5 years ago

It might need a storage abstraction so that yessql would only be a custom implementation.

AmrTealeb commented 5 years ago

@sfmskywalker I was trying to run this workflow Design_2 using this code await _workflowManager.StartWorkflowAsync(workflowType, activities.First(), input);

but the status is Faulted and the error message is "Error Specified argument was out of the range of valid values. Parameter name: column Actual value was 1."

Do you know what can be the cause for this?

AmrTealeb commented 5 years ago

@sfmskywalker Also i have another question about the workflow in previous comment When i run it the status is always either Finished or Faulted and never Idle or Halted Shouldn't the Signal Event halt the workflow until the signal is provided?

sfmskywalker commented 5 years ago

@AmrTealeb A workflow will be halted when execution enters an event activity (such as Signal) only if it's not the start of the workflow. If an event is the start of a workflow, the workflow will execute when the event is triggered. The reason it works this way is to allow an event on a workflow to actually cause that workflow to run when the event is triggered, and to halt the workflow when an event is encountered later on. When that happens, workflow execution resumes once the event is triggered that is blocking the workflow instance.

So that's why your workflow executes immediately: it starts with a Signal activity (an event), so invoking StartWorkflowAsync will cause your workflow to start.

However, you should not have to invoke the workflow yourself at all - the whole point of having "event" activities is so that the workflows will execute automatically when the appropriate event is triggered. For example, when you add a Signal event to your workflow, that workflow will automatically start as soon as that signal is triggered. The class that is responsible for this will internally invoke TriggerEventAsync, which in turn will call StartWorkflowAsync and ResumeWorkflowAsync to start new workflows and resume existing workflows, respectively.

If you are manually invoking workflows yourself using StartWorkflowAsync, then there's no point to start your workflow with a Signal (Input Event in your example) - you could just have it start with Set Booking Property. But if you do want to execute your workflow in response to a Signal, then your workflow is fine and you should not do this: await _workflowManager.StartWorkflowAsync(workflowType, activities.First(), input);. Instead, your application should trigger the Signal event by making an HTTP request to the HttpWorkflowController's Trigger action.

Alternatively, if you prefer to trigger a workflow yourself from your own controller that is invoked from your application, then you could come up with your own event activity and use its name when invoking _workflowManager.TriggerEventAsync(). However, you might just as well use the existing HttpWorkflowController and use it to first generate a URL so that you can encode the signal (to prevent tampering), and use that URL to trigger the workflow over HTTP.

To summarize:

Hope this helps, but let me know if anything is unclear.

AmrTealeb commented 5 years ago

@sfmskywalker Thank you for your help I removed the HttpRequestEvent , now when i run the workflow it is Halted as expected but when i trigger the event using await _workflowManager.ResumeWorkflowAsync(workflow, activity, input); The workflow is Faulted with error message Unable to cast object of type 'OrchardCore.DisplayManagement.Liquid.LiquidViewTemplate' to type 'Esprima.Ast.Program'.

AmrTealeb commented 4 years ago

Is there a way i make the CMS use my implementation for IWorkflowTypeStore and IWorkflowStore ?

sfmskywalker commented 4 years ago

Yes, you should be able to register your own implementation using e.g. services.Replace(ServiceDescriptor.Scoped(typeof(IWorkflowTypeStore), typeof(MyCustomWorkflowTypeStore)) (I haven't checked this in detail, this is just to give you an idea) and making sure your feature depends on the workflows feature so that your Startup runs after the Workflow's one. This works the same way for any other Orchard service you wish to provide a custom implementation for.

sebastienros commented 4 years ago

You shouldn't even have to "replace" it. Just register another implementation and the DI will use the latest one. As long as your feature depends on the Workflow one.

sfmskywalker commented 4 years ago

That's true. Just out of curiosity, when would one use the Replace extension method? I've seen it used in the OpenId EF module.

jtkech commented 4 years ago

This is useful when we inject an IEnumerable<ISomething> or use .GetServices<ISomething>() and we don't want that the one we override will get collected.

sfmskywalker commented 4 years ago

That makes sense. Thanks!

AmrTealeb commented 4 years ago

Is there a way i can use orchard workflow designer as a stand alone feature? in other words, include the designer in my project without the CMS?

sfmskywalker commented 4 years ago

You should be able to do so , but you would still depend on the Orchard Core Framework. The Workflows module depends on Orchard's drivers & shape systems for example.

AmrTealeb commented 4 years ago

Yes, we are planning to use Orchard Core Framework with a business application project