Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generic extensions to create orchestrations in a strongly-typed manner #714

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

rozhnovl
Copy link

Adding some extensions to allow creation of Orchestrations/Activities in a type-safe manner.
Basically, a bit of modernizing API without breaking the backwards compatibility + checking in compile time that the parameter passed matches the orchestration/activity contracts.
Samples below based on Tests in project.
Before #1
OrchestrationInstance id = await this.client.CreateOrchestrationInstanceAsync(typeof(AsyncGreetingsOrchestration), null);
After #1
OrchestrationInstance id = await this.client.CreateOrchestrationInstanceAsync<AsyncGreetingsOrchestration>(null);
Before #2 (OK)
OrchestrationInstance id = await this.client.CreateOrchestrationInstanceAsync(typeof (TypeMissingOrchestration), "test");
Before #2 (Fails in Runtime)
OrchestrationInstance id = await this.client.CreateOrchestrationInstanceAsync(typeof (TypeMissingOrchestration), 1);
After #2 (Fails to Compile)
OrchestrationInstance id = await this.client.CreateOrchestrationInstanceAsync<TypeMissingOrchestration>(1);

@ghost
Copy link

ghost commented Apr 25, 2022

CLA assistant check
All CLA requirements met.

@cgillum
Copy link
Member

cgillum commented Apr 26, 2022

Looks nice! Can you share some examples of what it will look like to use the OrchestrationContextExtensions? The examples above only cover creating new orchestration instances.

@rozhnovl
Copy link
Author

Nearly the same, except for the fact that we need to include both input and result types in type parameters:
Before:
context.ScheduleTask<string>(typeof(Activities.Echo), input);
After:
context.ScheduleTask<Activities.Echo, string, string>(input);

@cgillum
Copy link
Member

cgillum commented Apr 26, 2022

Would you consider creating similar extensions for the other orchestration context APIs, like ScheduleTaskWithRetry and the sub-orchestration APIs?

…lient functionality with generic implementations
@rozhnovl
Copy link
Author

Sure, done.

@rozhnovl
Copy link
Author

rozhnovl commented May 6, 2022

@cgillum do you want anything else included into this PR?

@jviau
Copy link
Collaborator

jviau commented May 12, 2022

I am not against these extensions on their own, but I would recommend taking a look at the pattern MediatR uses. This is what we have adopted in our usage of DTFx. We defined some contracts which acts as both the input of the orchestration or activity and the description of what orchestration or activity is to be ran. In the end all the APIs get unified to a SendAsync pattern.

Some examples:

public static class OrchestrationContextExtensions
{
    public static Task<TResult> SendAsync(this OrchestrationContext context, IOrchestrationRequest<TResult> request)
    {
        return context.CreateSubOrchestrationInstance<TResult>(request.Descriptor.Name, request.Descriptor.Version, request);
    }

    public static Task<TResult> SendAsync(this OrchestrationContext context, IActivityRequest<TResult> request)
    {
        return context.ScheduleTask<TResult>(request.Descriptor.Name, request.Descriptor.Version, request);
    }
}

public class MyOrchestration : IOrchestrationRequest<MyResult>
{
    // this object is the input itself.

   // This is part of the request contract. It will not be serialized and is used in the `SendAsync` implementation to get the Name and Version of the orcherstration to enqueue.
   public TaskDescriptor Descriptor => new(typeof(Handler));

  public class Handler : OrchestrationBase<MyOrchestration, MyResult>
  {
    public override async Task<MyResult> RunAsync(MyOrchestration input)
    {
        // Context is now a property. Invoke any sub orchestration or activity via 'SendAsync()'
        MyActivityResult activityResult = await  Context.SendAsync(new MyActivity(/* whatever input this needs */));
       
       MyOtherOrchestrationResult orchestrationResult = await Context.SendAsync(new MyOtherOrchestration(/* ctor params */));
       return new MyResult();
    }
  }
}

public class MyActivity : IActivityRequest<MyActivityResult>
{
    // this object is the input itself.

    // This is part of the request contract. It will not be serialized and is used in the `SendAsync` implementation to get the Name and Version of the activity to enqueue.
   public TaskDescriptor Descriptor => new(typeof(Handler));

  public class Handler : ActivityBase<MyActivity, MyActivityResult>
  {
    public override Task<MyActivityResult> RunAsync(MyActivity input)
    {
        return Task.FromResult(new MyActivityResult());
    }
  }
}

// also TaskHubClient works with this as well:
OrchestrationInstance instance = await client.StartOrchestrationAsync(new MyOrchestration(/* ctor params */));

This pattern works very well for us for a few reasons:

  1. Overload resolution - 1 API SendAsync, but can use generic constraints to resolve to all sorts of overloads with custom logic.
    • We have an overload which takes in activities that kick off Azure long running operations and automatically wrap it in an orchestration that will perform LRO polling. No need to know what the polling orchestration is! Just pass in the activity request.
    • Can add the RetryOptions variants as well via additional overloads.
  2. Can move more and more logic into the base request types and deal with it in the SendAsync or base class implementations.
    • Want an activity or orchestration to declare their own retry options? Throw a IRetryableTask contract with a RetryOptions property and have SendAsync test for that implementation and then it can now call the retryable method on OrchestrationContext.
    • Timeout? Add a IWithTimeout contact, and then the OrchestrationBase or ActivityBase implementation can handle a cooperative cancellation token details.
  3. Generic param constraints are all implicit. With C#, either all generic params are implicit or all need to be explicitly declared, no mixing of implicit and explicit. With your solution above, users will almost always need to explicitly list all generic params.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants