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