diff --git a/src/DurableTask.Core/NameVersionHelper.cs b/src/DurableTask.Core/NameVersionHelper.cs
index f45ad94c0..52e09c88d 100644
--- a/src/DurableTask.Core/NameVersionHelper.cs
+++ b/src/DurableTask.Core/NameVersionHelper.cs
@@ -14,7 +14,9 @@
namespace DurableTask.Core
{
using System;
+ using System.Collections.Generic;
using System.Dynamic;
+ using System.Linq;
using System.Reflection;
///
@@ -99,5 +101,56 @@ internal static string GetFullyQualifiedMethodName(string declaringType, string
return declaringType + "." + methodName;
}
+
+ ///
+ /// Gets the fully qualified method name by joining a prefix representing the declaring type and a suffix representing the parameter list.
+ /// For example,
+ /// "DurableTask.Emulator.Tests.EmulatorFunctionalTests+IInheritedTestOrchestrationTasksB`2[System.Int32,System.String].Juggle.(Int32,Boolean)"
+ /// would be the result for the method `Juggle(int, bool)` as member of
+ /// generic type interface declared like `DurableTask.Emulator.Tests.EmulatorFunctionalTests.IInheritedTestOrchestrationTasksB{int, string}`,
+ /// even if the method were inherited from a base interface.
+ ///
+ /// typically the result of call to Type.ToString(): Type.FullName is more verbose
+ ///
+ ///
+ internal static string GetFullyQualifiedMethodName(string declaringType, MethodInfo methodInfo)
+ {
+ IEnumerable paramTypeNames = methodInfo.GetParameters().Select(x => x.ParameterType.Name);
+ string paramTypeNamesCsv = string.Join(",", paramTypeNames);
+ string methodNameWithParameterList = $"{methodInfo.Name}.({paramTypeNamesCsv})";
+ return GetFullyQualifiedMethodName(declaringType, methodNameWithParameterList);
+ }
+
+ ///
+ /// Gets all methods from an interface, including those inherited from a base interface
+ ///
+ ///
+ ///
+ ///
+ ///
+ internal static IList GetAllInterfaceMethods(Type t, Func getMethodUniqueId, HashSet visited = null)
+ {
+ if (visited == null)
+ {
+ visited = new HashSet();
+ }
+ List result = new List();
+ foreach (MethodInfo m in t.GetMethods())
+ {
+ string name = getMethodUniqueId(m);
+ if (!visited.Contains(name))
+ {
+ // In some cases, such as when a generic type interface inherits an interface with the same name, Task.GetMethod includes the methods from the base interface.
+ // This check is to avoid dupicates from these.
+ result.Add(m);
+ visited.Add(name);
+ }
+ }
+ foreach (Type baseInterface in t.GetInterfaces())
+ {
+ result.AddRange(GetAllInterfaceMethods(baseInterface, getMethodUniqueId, visited: visited));
+ }
+ return result;
+ }
}
}
\ No newline at end of file
diff --git a/src/DurableTask.Core/OrchestrationContext.cs b/src/DurableTask.Core/OrchestrationContext.cs
index 290fc9ae1..5a48eb93f 100644
--- a/src/DurableTask.Core/OrchestrationContext.cs
+++ b/src/DurableTask.Core/OrchestrationContext.cs
@@ -91,27 +91,25 @@ public virtual T CreateClient() where T : class
/// If true, the method name translation from the interface contains
/// the interface name, if false then only the method name is used
///
+ ///
+ /// This is deprecated and exists only for back-compatibility.
+ /// See , which adds support for C# interface features such as inheritance, generics, and method overloading.
+ ///
///
public virtual T CreateClient(bool useFullyQualifiedMethodNames) where T : class
{
- if (!typeof(T).IsInterface && !typeof(T).IsClass)
- {
- throw new InvalidOperationException($"{nameof(T)} must be an interface or class.");
- }
-
- IInterceptor scheduleProxy = new ScheduleProxy(this, useFullyQualifiedMethodNames);
-
- if (typeof(T).IsClass)
- {
- if (typeof(T).IsSealed)
- {
- throw new InvalidOperationException("Class cannot be sealed.");
- }
-
- return ProxyGenerator.CreateClassProxy(scheduleProxy);
- }
+ return CreateClient(() => new ScheduleProxy(this, useFullyQualifiedMethodNames));
+ }
- return ProxyGenerator.CreateInterfaceProxyWithoutTarget(scheduleProxy);
+ ///
+ /// Create a proxy client class to schedule remote TaskActivities via a strongly typed interface.
+ ///
+ ///
+ ///
+ ///
+ public virtual T CreateClientV2() where T : class
+ {
+ return CreateClient(() => new ScheduleProxyV2(this, typeof(T).ToString()));
}
///
@@ -410,5 +408,33 @@ public abstract Task CreateSubOrchestrationInstance(string name, string ve
/// the first execution of this orchestration instance.
///
public abstract void ContinueAsNew(string newVersion, object input);
+
+ ///
+ /// Create a proxy client class to schedule remote TaskActivities via a strongly typed interface.
+ ///
+ ///
+ ///
+ ///
+ private static T CreateClient(Func createScheduleProxy) where T : class
+ {
+ if (!typeof(T).IsInterface && !typeof(T).IsClass)
+ {
+ throw new InvalidOperationException($"{nameof(T)} must be an interface or class.");
+ }
+
+ IInterceptor scheduleProxy = createScheduleProxy();
+
+ if (typeof(T).IsClass)
+ {
+ if (typeof(T).IsSealed)
+ {
+ throw new InvalidOperationException("Class cannot be sealed.");
+ }
+
+ return ProxyGenerator.CreateClassProxy(scheduleProxy);
+ }
+
+ return ProxyGenerator.CreateInterfaceProxyWithoutTarget(scheduleProxy);
+ }
}
}
\ No newline at end of file
diff --git a/src/DurableTask.Core/ScheduleProxy.cs b/src/DurableTask.Core/ScheduleProxy.cs
index 7f21d2037..4922d2cf1 100644
--- a/src/DurableTask.Core/ScheduleProxy.cs
+++ b/src/DurableTask.Core/ScheduleProxy.cs
@@ -21,6 +21,10 @@ namespace DurableTask.Core
using Castle.DynamicProxy;
using DurableTask.Core.Common;
+ ///
+ /// This is deprecated and exists only for back-compatibility.
+ /// See , which adds support for C# interface features such as inheritance, generics, and method overloading.
+ ///
internal class ScheduleProxy : IInterceptor
{
private readonly OrchestrationContext context;
@@ -64,7 +68,7 @@ public void Intercept(IInvocation invocation)
arguments.Add(new Utils.TypeMetadata { AssemblyName = typeArg.Assembly.FullName!, FullyQualifiedTypeName = typeArg.FullName });
}
- string normalizedMethodName = NameVersionHelper.GetDefaultName(invocation.Method, this.useFullyQualifiedMethodNames);
+ string normalizedMethodName = this.NormalizeMethodName(invocation.Method);
if (returnType == typeof(Task))
{
@@ -93,5 +97,10 @@ public void Intercept(IInvocation invocation)
return;
}
+
+ protected virtual string NormalizeMethodName(MethodInfo method)
+ {
+ return NameVersionHelper.GetDefaultName(method, this.useFullyQualifiedMethodNames);
+ }
}
}
\ No newline at end of file
diff --git a/src/DurableTask.Core/ScheduleProxyV2.cs b/src/DurableTask.Core/ScheduleProxyV2.cs
new file mode 100644
index 000000000..22e078dc6
--- /dev/null
+++ b/src/DurableTask.Core/ScheduleProxyV2.cs
@@ -0,0 +1,35 @@
+// ----------------------------------------------------------------------------------
+// Copyright Microsoft Corporation
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ----------------------------------------------------------------------------------
+
+namespace DurableTask.Core
+{
+ using System.Reflection;
+ using Castle.DynamicProxy;
+
+ internal class ScheduleProxyV2 : ScheduleProxy, IInterceptor
+ {
+ private readonly string declaringTypeFullName;
+
+ public ScheduleProxyV2(OrchestrationContext context, string declaringTypeFullName)
+ : base(context)
+ {
+ this.declaringTypeFullName = declaringTypeFullName;
+ }
+
+ protected override string NormalizeMethodName(MethodInfo method)
+ {
+ // uses declaring type defined externally because MethodInfo members, such as Method.DeclaringType, could return the base type that the method inherits from
+ return string.IsNullOrEmpty(this.declaringTypeFullName) ? method.Name : NameVersionHelper.GetFullyQualifiedMethodName(this.declaringTypeFullName, method);
+ }
+ }
+}
diff --git a/src/DurableTask.Core/TaskHubWorker.cs b/src/DurableTask.Core/TaskHubWorker.cs
index 76883e5ce..24271dc78 100644
--- a/src/DurableTask.Core/TaskHubWorker.cs
+++ b/src/DurableTask.Core/TaskHubWorker.cs
@@ -439,6 +439,10 @@ public TaskHubWorker AddTaskActivities(params ObjectCreator[] task
/// and version set to an empty string. Methods can then be invoked from task orchestrations
/// by calling ScheduleTask(name, version) with name as the method name and string.Empty as the version.
///
+ ///
+ /// This is deprecated and exists only for back-compatibility.
+ /// See , which adds support for C# interface features such as inheritance, generics, and method overloading.
+ ///
/// Interface
/// Object that implements this interface
public TaskHubWorker AddTaskActivitiesFromInterface(T activities)
@@ -452,6 +456,10 @@ public TaskHubWorker AddTaskActivitiesFromInterface(T activities)
/// and version set to an empty string. Methods can then be invoked from task orchestrations
/// by calling ScheduleTask(name, version) with name as the method name and string.Empty as the version.
///
+ ///
+ /// This is deprecated and exists only for back-compatibility.
+ /// See , which adds support for C# interface features such as inheritance, generics, and method overloading.
+ ///
/// Interface
/// Object that implements this interface
///
@@ -469,6 +477,23 @@ public TaskHubWorker AddTaskActivitiesFromInterface(T activities, bool useFul
/// and version set to an empty string. Methods can then be invoked from task orchestrations
/// by calling ScheduleTask(name, version) with name as the method name and string.Empty as the version.
///
+ /// Interface
+ /// Object that implements this interface
+ public TaskHubWorker AddTaskActivitiesFromInterfaceV2(object activities)
+ {
+ return this.AddTaskActivitiesFromInterfaceV2(typeof(T), activities);
+ }
+
+ ///
+ /// Infers and adds every method in the specified interface T on the
+ /// passed in object as a different TaskActivity with Name set to the method name
+ /// and version set to an empty string. Methods can then be invoked from task orchestrations
+ /// by calling ScheduleTask(name, version) with name as the method name and string.Empty as the version.
+ ///
+ ///
+ /// This is deprecated and exists only for back-compatibility.
+ /// See , which adds support for C# interface features such as inheritance, generics, and method overloading.
+ ///
/// Interface type.
/// Object that implements the interface
///
@@ -477,16 +502,7 @@ public TaskHubWorker AddTaskActivitiesFromInterface(T activities, bool useFul
///
public TaskHubWorker AddTaskActivitiesFromInterface(Type @interface, object activities, bool useFullyQualifiedMethodNames = false)
{
- if (!@interface.IsInterface)
- {
- throw new Exception("Contract can only be an interface.");
- }
-
- if (!@interface.IsAssignableFrom(activities.GetType()))
- {
- throw new ArgumentException($"{activities.GetType().FullName} does not implement {@interface.FullName}", nameof(activities));
- }
-
+ this.ValidateActivitiesInterfaceType(@interface, activities);
foreach (MethodInfo methodInfo in @interface.GetMethods())
{
TaskActivity taskActivity = new ReflectionBasedTaskActivity(activities, methodInfo);
@@ -500,6 +516,29 @@ public TaskHubWorker AddTaskActivitiesFromInterface(Type @interface, object acti
return this;
}
+ ///
+ /// Infers and adds every method in the specified interface T on the
+ /// passed in object as a different TaskActivity with Name set to the method name
+ /// and version set to an empty string. Methods can then be invoked from task orchestrations
+ /// by calling ScheduleTask(name, version) with name as the method name and string.Empty as the version.
+ ///
+ /// Interface type.
+ /// Object that implements the interface
+ public TaskHubWorker AddTaskActivitiesFromInterfaceV2(Type @interface, object activities)
+ {
+ this.ValidateActivitiesInterfaceType(@interface, activities);
+ var methods = NameVersionHelper.GetAllInterfaceMethods(@interface, (MethodInfo m) => NameVersionHelper.GetFullyQualifiedMethodName(@interface.ToString(), m));
+ foreach (MethodInfo methodInfo in methods)
+ {
+ TaskActivity taskActivity = new ReflectionBasedTaskActivity(activities, methodInfo);
+ string name = NameVersionHelper.GetFullyQualifiedMethodName(@interface.ToString(), methodInfo);
+ ObjectCreator creator = new NameValueObjectCreator(name, NameVersionHelper.GetDefaultVersion(methodInfo), taskActivity);
+ this.AddTaskActivities(creator);
+ }
+
+ return this;
+ }
+
///
/// Infers and adds every method in the specified interface or class T on the
/// passed in object as a different TaskActivity with Name set to the method name
@@ -590,5 +629,18 @@ public void Dispose()
{
((IDisposable)this.slimLock).Dispose();
}
+
+ private void ValidateActivitiesInterfaceType(Type @interface, object activities)
+ {
+ if (!@interface.IsInterface)
+ {
+ throw new Exception("Contract can only be an interface.");
+ }
+
+ if (!@interface.IsAssignableFrom(activities.GetType()))
+ {
+ throw new ArgumentException($"type {activities.GetType().FullName} does not implement {@interface.FullName}");
+ }
+ }
}
}
\ No newline at end of file
diff --git a/test/DurableTask.Emulator.Tests/EmulatorFunctionalTests.cs b/test/DurableTask.Emulator.Tests/EmulatorFunctionalTests.cs
index 564de7a3a..0ba937bbd 100644
--- a/test/DurableTask.Emulator.Tests/EmulatorFunctionalTests.cs
+++ b/test/DurableTask.Emulator.Tests/EmulatorFunctionalTests.cs
@@ -314,11 +314,163 @@ await worker.AddTaskOrchestrations(orchestrationType)
await worker.StopAsync(true);
}
+ [TestMethod]
+ public async Task RegisterOrchestrationTasksFromInterface_InterfaceUsingInheritanceGenericsMethodOverloading_OrchestrationSuccess()
+ {
+ var orchestrationService = new LocalOrchestrationService();
+
+ var worker = new TaskHubWorker(orchestrationService);
+ await worker.AddTaskOrchestrations(typeof(TestInheritedTasksOrchestration))
+ .AddTaskActivitiesFromInterfaceV2(new InheritedTestOrchestrationTasksA())
+ .AddTaskActivitiesFromInterfaceV2(typeof(IInheritedTestOrchestrationTasksB), new InheritedTestOrchestrationTasksB())
+ .StartAsync();
+
+ var client = new TaskHubClient(orchestrationService);
+ OrchestrationInstance id = await client.CreateOrchestrationInstanceAsync(typeof(TestInheritedTasksOrchestration),
+ null);
+
+ var state = await client.WaitForOrchestrationAsync(id, TimeSpan.FromSeconds(30), CancellationToken.None);
+ Assert.AreEqual(OrchestrationStatus.Completed, state.OrchestrationStatus);
+
+ Assert.AreEqual(InheritedTestOrchestrationTasksA.BumbleResult, TestInheritedTasksOrchestration.BumbleResultA);
+ Assert.AreEqual(InheritedTestOrchestrationTasksA.WobbleResult, TestInheritedTasksOrchestration.WobbleResultA);
+ Assert.AreEqual(InheritedTestOrchestrationTasksA.DerivedTaskResult, TestInheritedTasksOrchestration.DerivedTaskResultA);
+ Assert.AreEqual(InheritedTestOrchestrationTasksA.JuggleResult, TestInheritedTasksOrchestration.JuggleResultA);
+
+ Assert.AreEqual(InheritedTestOrchestrationTasksB.BumbleResult, TestInheritedTasksOrchestration.BumbleResultB);
+ Assert.AreEqual(InheritedTestOrchestrationTasksB.WobbleResult, TestInheritedTasksOrchestration.WobbleResultB);
+ Assert.AreEqual(InheritedTestOrchestrationTasksB.OverloadedWobbleResult1, TestInheritedTasksOrchestration.OverloadedWobbleResult1B);
+ Assert.AreEqual(InheritedTestOrchestrationTasksB.OverloadedWobbleResult2, TestInheritedTasksOrchestration.OverloadedWobbleResult2B);
+ Assert.AreEqual(InheritedTestOrchestrationTasksB.JuggleResult, TestInheritedTasksOrchestration.JuggleResultB);
+ }
+
private static void AssertTagsEqual(IDictionary expectedTags, IDictionary actualTags)
{
Assert.IsNotNull(actualTags);
Assert.AreEqual(expectedTags.Count, actualTags.Count);
Assert.IsTrue(expectedTags.All(tag => actualTags.TryGetValue(tag.Key, out var value) && value == tag.Value));
}
+
+ // base interface without generic type parameters
+ public interface IBaseTestOrchestrationTasks
+ {
+ Task Juggle(int toss, bool withFlair);
+ }
+
+ // generic type base interface inheriting non-generic type with same name
+ public interface IBaseTestOrchestrationTasks : IBaseTestOrchestrationTasks
+ {
+ Task Bumble(TIn fumble, bool likeAKlutz);
+ Task Wobble(TIn jiggle, bool withGusto);
+ }
+
+ // interface with derived task
+ public interface IInheritedTestOrchestrationTasksA : IBaseTestOrchestrationTasks
+ {
+ Task DerivedTask(int i);
+ }
+
+ // interface with overloaded method
+ public interface IInheritedTestOrchestrationTasksB : IBaseTestOrchestrationTasks
+ {
+ // this method overloads methods from both inherited interface and this interface
+ Task Wobble(TIn name);
+ Task Wobble(string id, TIn subId);
+ }
+
+ public class InheritedTestOrchestrationTasksA : IInheritedTestOrchestrationTasksA
+ {
+ public const string BumbleResult = nameof(Bumble) + "-A";
+ public const string WobbleResult = nameof(Wobble) + "-A";
+ public const string DerivedTaskResult = nameof(DerivedTask) + "-A";
+ public const int JuggleResult = 419;
+
+ public Task Bumble(string fumble, bool likeAKlutz)
+ {
+ return Task.FromResult(BumbleResult);
+ }
+
+ public Task Wobble(string jiggle, bool withGusto)
+ {
+ return Task.FromResult(WobbleResult);
+ }
+
+ public Task DerivedTask(int i)
+ {
+ return Task.FromResult(DerivedTaskResult);
+ }
+
+ public Task Juggle(int toss, bool withFlair)
+ {
+ return Task.FromResult(JuggleResult);
+ }
+ }
+
+ public class InheritedTestOrchestrationTasksB : IInheritedTestOrchestrationTasksB
+ {
+ public const string BumbleResult = nameof(Bumble) + "-B";
+ public const string WobbleResult = nameof(Wobble) + "-B";
+ public const string OverloadedWobbleResult1 = nameof(Wobble) + "-B-overloaded-1";
+ public const string OverloadedWobbleResult2 = nameof(Wobble) + "-B-overloaded-2";
+ public const int JuggleResult = 420;
+
+ public Task Bumble(int fumble, bool likeAKlutz)
+ {
+ return Task.FromResult(BumbleResult);
+ }
+
+ public Task Wobble(int jiggle, bool withGusto)
+ {
+ return Task.FromResult(WobbleResult);
+ }
+
+ public Task Wobble(string id, int subId)
+ {
+ return Task.FromResult(OverloadedWobbleResult1);
+ }
+
+ public Task Wobble(int id)
+ {
+ return Task.FromResult(OverloadedWobbleResult2);
+ }
+
+ public Task Juggle(int toss, bool withFlair)
+ {
+ return Task.FromResult(JuggleResult);
+ }
+ }
+
+ public class TestInheritedTasksOrchestration : TaskOrchestration
+ {
+ // HACK: This is just a hack to communicate result of orchestration back to test
+ public static string BumbleResultA;
+ public static string WobbleResultA;
+ public static string DerivedTaskResultA;
+ public static int JuggleResultA;
+ public static string BumbleResultB;
+ public static string WobbleResultB;
+ public static string OverloadedWobbleResult1B;
+ public static string OverloadedWobbleResult2B;
+ public static int JuggleResultB;
+
+ public override async Task RunTask(OrchestrationContext context, string input)
+ {
+ var tasksA = context.CreateClientV2();
+ var tasksB = context.CreateClientV2>();
+
+ BumbleResultA = await tasksA.Bumble(string.Empty, false);
+ WobbleResultA = await tasksA.Wobble(string.Empty, false);
+ DerivedTaskResultA = await tasksA.DerivedTask(0);
+ JuggleResultA = await tasksA.Juggle(1, true);
+
+ BumbleResultB = await tasksB.Bumble(0, false);
+ WobbleResultB = await tasksB.Wobble(-1, false);
+ OverloadedWobbleResult1B = await tasksB.Wobble("a", 2);
+ OverloadedWobbleResult2B = await tasksB.Wobble(1);
+ JuggleResultB = await tasksB.Juggle(1, true);
+
+ return string.Empty;
+ }
+ }
}
}
\ No newline at end of file