From 846d65a561a390ecbc1eba0b143f8ad7e230c621 Mon Sep 17 00:00:00 2001 From: Kevin J Xu Date: Thu, 28 Dec 2023 19:00:25 -0800 Subject: [PATCH 1/4] support task interface with inheritance, generics, and method overloading --- DurableTask.sln | 2 +- src/DurableTask.Core/NameVersionHelper.cs | 42 +++++ src/DurableTask.Core/OrchestrationContext.cs | 60 +++++-- src/DurableTask.Core/ScheduleProxy.cs | 11 +- src/DurableTask.Core/ScheduleProxyV2.cs | 34 ++++ src/DurableTask.Core/TaskHubWorker.cs | 72 +++++++-- .../EmulatorFunctionalTests.cs | 152 ++++++++++++++++++ 7 files changed, 344 insertions(+), 29 deletions(-) create mode 100644 src/DurableTask.Core/ScheduleProxyV2.cs diff --git a/DurableTask.sln b/DurableTask.sln index 4d57ec5e2..b2db5a2a6 100644 --- a/DurableTask.sln +++ b/DurableTask.sln @@ -320,7 +320,7 @@ Global {D818ED4C-29B9-431F-8D09-EE8C82510E25} = {240FA679-D5A7-41CA-BA22-70B45A966088} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {2D63A120-9394-48D9-8CA9-1184364FB854} EnterpriseLibraryConfigurationToolBinariesPath = packages\TransientFaultHandling.Core.5.1.1209.1\lib\NET4 + SolutionGuid = {2D63A120-9394-48D9-8CA9-1184364FB854} EndGlobalSection EndGlobal diff --git a/src/DurableTask.Core/NameVersionHelper.cs b/src/DurableTask.Core/NameVersionHelper.cs index f45ad94c0..a940d52e7 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,45 @@ internal static string GetFullyQualifiedMethodName(string declaringType, string return declaringType + "." + methodName; } + + 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..ea6591c49 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 this.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 this.CreateClient(() => new ScheduleProxyV2(this, typeof(T).FullName)); } /// @@ -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 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..55089906a --- /dev/null +++ b/src/DurableTask.Core/ScheduleProxyV2.cs @@ -0,0 +1,34 @@ +// ---------------------------------------------------------------------------------- +// 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) + { + 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..5d19bde48 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.FullName, m)); + foreach (MethodInfo methodInfo in methods) + { + TaskActivity taskActivity = new ReflectionBasedTaskActivity(activities, methodInfo); + string name = NameVersionHelper.GetFullyQualifiedMethodName(@interface.FullName, 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..79c35ecea 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(string name); + Task Wobble(int id); + } + + 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(string fumble, bool likeAKlutz) + { + return Task.FromResult(BumbleResult); + } + + public Task Wobble(string jiggle, bool withGusto) + { + return Task.FromResult(WobbleResult); + } + + public Task Wobble(string name) + { + 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(string.Empty, false); + WobbleResultB = await tasksB.Wobble(string.Empty, false); + OverloadedWobbleResult1B = await tasksB.Wobble("name"); + OverloadedWobbleResult2B = await tasksB.Wobble(1); + JuggleResultB = await tasksB.Juggle(1, true); + + return string.Empty; + } + } } } \ No newline at end of file From ccabcff60b0b37d1e313cd114b990b44c8b9100f Mon Sep 17 00:00:00 2001 From: Kevin J Xu Date: Thu, 28 Dec 2023 20:42:06 -0800 Subject: [PATCH 2/4] revert auto diff in unrelated file --- DurableTask.sln | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DurableTask.sln b/DurableTask.sln index b2db5a2a6..4d57ec5e2 100644 --- a/DurableTask.sln +++ b/DurableTask.sln @@ -320,7 +320,7 @@ Global {D818ED4C-29B9-431F-8D09-EE8C82510E25} = {240FA679-D5A7-41CA-BA22-70B45A966088} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution - EnterpriseLibraryConfigurationToolBinariesPath = packages\TransientFaultHandling.Core.5.1.1209.1\lib\NET4 SolutionGuid = {2D63A120-9394-48D9-8CA9-1184364FB854} + EnterpriseLibraryConfigurationToolBinariesPath = packages\TransientFaultHandling.Core.5.1.1209.1\lib\NET4 EndGlobalSection EndGlobal From 0570858d568b8d8031d56f8b317d1636867113eb Mon Sep 17 00:00:00 2001 From: Kevin J Xu Date: Tue, 9 Jan 2024 22:54:18 -0800 Subject: [PATCH 3/4] add test case using generic type arguments in registration --- .../EmulatorFunctionalTests.cs | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/test/DurableTask.Emulator.Tests/EmulatorFunctionalTests.cs b/test/DurableTask.Emulator.Tests/EmulatorFunctionalTests.cs index 79c35ecea..0ba937bbd 100644 --- a/test/DurableTask.Emulator.Tests/EmulatorFunctionalTests.cs +++ b/test/DurableTask.Emulator.Tests/EmulatorFunctionalTests.cs @@ -322,7 +322,7 @@ public async Task RegisterOrchestrationTasksFromInterface_InterfaceUsingInherita var worker = new TaskHubWorker(orchestrationService); await worker.AddTaskOrchestrations(typeof(TestInheritedTasksOrchestration)) .AddTaskActivitiesFromInterfaceV2(new InheritedTestOrchestrationTasksA()) - .AddTaskActivitiesFromInterfaceV2(typeof(IInheritedTestOrchestrationTasksB), new InheritedTestOrchestrationTasksB()) + .AddTaskActivitiesFromInterfaceV2(typeof(IInheritedTestOrchestrationTasksB), new InheritedTestOrchestrationTasksB()) .StartAsync(); var client = new TaskHubClient(orchestrationService); @@ -371,11 +371,11 @@ public interface IInheritedTestOrchestrationTasksA : IBaseTestOrchestrationTasks } // interface with overloaded method - public interface IInheritedTestOrchestrationTasksB : IBaseTestOrchestrationTasks + public interface IInheritedTestOrchestrationTasksB : IBaseTestOrchestrationTasks { // this method overloads methods from both inherited interface and this interface - Task Wobble(string name); - Task Wobble(int id); + Task Wobble(TIn name); + Task Wobble(string id, TIn subId); } public class InheritedTestOrchestrationTasksA : IInheritedTestOrchestrationTasksA @@ -406,7 +406,7 @@ public Task Juggle(int toss, bool withFlair) } } - public class InheritedTestOrchestrationTasksB : IInheritedTestOrchestrationTasksB + public class InheritedTestOrchestrationTasksB : IInheritedTestOrchestrationTasksB { public const string BumbleResult = nameof(Bumble) + "-B"; public const string WobbleResult = nameof(Wobble) + "-B"; @@ -414,17 +414,17 @@ public class InheritedTestOrchestrationTasksB : IInheritedTestOrchestrationTasks public const string OverloadedWobbleResult2 = nameof(Wobble) + "-B-overloaded-2"; public const int JuggleResult = 420; - public Task Bumble(string fumble, bool likeAKlutz) + public Task Bumble(int fumble, bool likeAKlutz) { return Task.FromResult(BumbleResult); } - public Task Wobble(string jiggle, bool withGusto) + public Task Wobble(int jiggle, bool withGusto) { return Task.FromResult(WobbleResult); } - public Task Wobble(string name) + public Task Wobble(string id, int subId) { return Task.FromResult(OverloadedWobbleResult1); } @@ -456,16 +456,16 @@ public class TestInheritedTasksOrchestration : TaskOrchestration public override async Task RunTask(OrchestrationContext context, string input) { var tasksA = context.CreateClientV2(); - var tasksB = 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(string.Empty, false); - WobbleResultB = await tasksB.Wobble(string.Empty, false); - OverloadedWobbleResult1B = await tasksB.Wobble("name"); + 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); From 63d17fe9c0144d63ffea33dc96d0b9f7bfadcc46 Mon Sep 17 00:00:00 2001 From: Kevin J Xu Date: Tue, 9 Jan 2024 23:15:22 -0800 Subject: [PATCH 4/4] shorten generic type argument list in fully qualified name; add comment --- src/DurableTask.Core/NameVersionHelper.cs | 11 +++++++++++ src/DurableTask.Core/OrchestrationContext.cs | 6 +++--- src/DurableTask.Core/ScheduleProxyV2.cs | 1 + src/DurableTask.Core/TaskHubWorker.cs | 4 ++-- 4 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/DurableTask.Core/NameVersionHelper.cs b/src/DurableTask.Core/NameVersionHelper.cs index a940d52e7..52e09c88d 100644 --- a/src/DurableTask.Core/NameVersionHelper.cs +++ b/src/DurableTask.Core/NameVersionHelper.cs @@ -102,6 +102,17 @@ 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); diff --git a/src/DurableTask.Core/OrchestrationContext.cs b/src/DurableTask.Core/OrchestrationContext.cs index ea6591c49..5a48eb93f 100644 --- a/src/DurableTask.Core/OrchestrationContext.cs +++ b/src/DurableTask.Core/OrchestrationContext.cs @@ -98,7 +98,7 @@ public virtual T CreateClient() where T : class /// public virtual T CreateClient(bool useFullyQualifiedMethodNames) where T : class { - return this.CreateClient(() => new ScheduleProxy(this, useFullyQualifiedMethodNames)); + return CreateClient(() => new ScheduleProxy(this, useFullyQualifiedMethodNames)); } /// @@ -109,7 +109,7 @@ public virtual T CreateClient(bool useFullyQualifiedMethodNames) where T : cl /// public virtual T CreateClientV2() where T : class { - return this.CreateClient(() => new ScheduleProxyV2(this, typeof(T).FullName)); + return CreateClient(() => new ScheduleProxyV2(this, typeof(T).ToString())); } /// @@ -415,7 +415,7 @@ public abstract Task CreateSubOrchestrationInstance(string name, string ve /// /// /// - private T CreateClient(Func createScheduleProxy) where T : class + private static T CreateClient(Func createScheduleProxy) where T : class { if (!typeof(T).IsInterface && !typeof(T).IsClass) { diff --git a/src/DurableTask.Core/ScheduleProxyV2.cs b/src/DurableTask.Core/ScheduleProxyV2.cs index 55089906a..22e078dc6 100644 --- a/src/DurableTask.Core/ScheduleProxyV2.cs +++ b/src/DurableTask.Core/ScheduleProxyV2.cs @@ -28,6 +28,7 @@ public ScheduleProxyV2(OrchestrationContext context, string declaringTypeFullNam 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 5d19bde48..24271dc78 100644 --- a/src/DurableTask.Core/TaskHubWorker.cs +++ b/src/DurableTask.Core/TaskHubWorker.cs @@ -527,11 +527,11 @@ public TaskHubWorker AddTaskActivitiesFromInterface(Type @interface, object acti public TaskHubWorker AddTaskActivitiesFromInterfaceV2(Type @interface, object activities) { this.ValidateActivitiesInterfaceType(@interface, activities); - var methods = NameVersionHelper.GetAllInterfaceMethods(@interface, (MethodInfo m) => NameVersionHelper.GetFullyQualifiedMethodName(@interface.FullName, m)); + 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.FullName, methodInfo); + string name = NameVersionHelper.GetFullyQualifiedMethodName(@interface.ToString(), methodInfo); ObjectCreator creator = new NameValueObjectCreator(name, NameVersionHelper.GetDefaultVersion(methodInfo), taskActivity); this.AddTaskActivities(creator); }