Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions src/DurableTask.Core/NameVersionHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@
namespace DurableTask.Core
{
using System;
using System.Collections.Generic;
using System.Dynamic;
using System.Linq;
using System.Reflection;

/// <summary>
Expand Down Expand Up @@ -99,5 +101,56 @@ internal static string GetFullyQualifiedMethodName(string declaringType, string

return declaringType + "." + methodName;
}

/// <summary>
/// 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.
/// </summary>
/// <param name="declaringType">typically the result of call to Type.ToString(): Type.FullName is more verbose</param>
/// <param name="methodInfo"></param>
/// <returns></returns>
internal static string GetFullyQualifiedMethodName(string declaringType, MethodInfo methodInfo)
{
IEnumerable<string> paramTypeNames = methodInfo.GetParameters().Select(x => x.ParameterType.Name);
string paramTypeNamesCsv = string.Join(",", paramTypeNames);
string methodNameWithParameterList = $"{methodInfo.Name}.({paramTypeNamesCsv})";
return GetFullyQualifiedMethodName(declaringType, methodNameWithParameterList);
}

/// <summary>
/// Gets all methods from an interface, including those inherited from a base interface
/// </summary>
/// <param name="t"></param>
/// <param name="getMethodUniqueId"></param>
/// <param name="visited"></param>
/// <returns></returns>
internal static IList<MethodInfo> GetAllInterfaceMethods(Type t, Func<MethodInfo, string> getMethodUniqueId, HashSet<string> visited = null)
{
if (visited == null)
{
visited = new HashSet<string>();
}
List<MethodInfo> result = new List<MethodInfo>();
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;
}
}
}
60 changes: 43 additions & 17 deletions src/DurableTask.Core/OrchestrationContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -91,27 +91,25 @@ public virtual T CreateClient<T>() 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
/// </param>
/// <remarks>
/// This is deprecated and exists only for back-compatibility.
/// See <see cref="CreateClientV2"/>, which adds support for C# interface features such as inheritance, generics, and method overloading.
/// </remarks>
/// <returns></returns>
public virtual T CreateClient<T>(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<T>(scheduleProxy);
}
return CreateClient<T>(() => new ScheduleProxy(this, useFullyQualifiedMethodNames));
}

return ProxyGenerator.CreateInterfaceProxyWithoutTarget<T>(scheduleProxy);
/// <summary>
/// Create a proxy client class to schedule remote TaskActivities via a strongly typed interface.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
/// <exception cref="InvalidOperationException"></exception>
public virtual T CreateClientV2<T>() where T : class
{
return CreateClient<T>(() => new ScheduleProxyV2(this, typeof(T).ToString()));
}

/// <summary>
Expand Down Expand Up @@ -410,5 +408,33 @@ public abstract Task<T> CreateSubOrchestrationInstance<T>(string name, string ve
/// the first execution of this orchestration instance.
/// </param>
public abstract void ContinueAsNew(string newVersion, object input);

/// <summary>
/// Create a proxy client class to schedule remote TaskActivities via a strongly typed interface.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
/// <exception cref="InvalidOperationException"></exception>
private static T CreateClient<T>(Func<IInterceptor> 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<T>(scheduleProxy);
}

return ProxyGenerator.CreateInterfaceProxyWithoutTarget<T>(scheduleProxy);
}
}
}
11 changes: 10 additions & 1 deletion src/DurableTask.Core/ScheduleProxy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ namespace DurableTask.Core
using Castle.DynamicProxy;
using DurableTask.Core.Common;

/// <remarks>
/// This is deprecated and exists only for back-compatibility.
/// See <see cref="ScheduleProxyV2"/>, which adds support for C# interface features such as inheritance, generics, and method overloading.
/// </remarks>
internal class ScheduleProxy : IInterceptor
{
private readonly OrchestrationContext context;
Expand Down Expand Up @@ -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))
{
Expand Down Expand Up @@ -93,5 +97,10 @@ public void Intercept(IInvocation invocation)

return;
}

protected virtual string NormalizeMethodName(MethodInfo method)
{
return NameVersionHelper.GetDefaultName(method, this.useFullyQualifiedMethodNames);
}
}
}
35 changes: 35 additions & 0 deletions src/DurableTask.Core/ScheduleProxyV2.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
72 changes: 62 additions & 10 deletions src/DurableTask.Core/TaskHubWorker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,10 @@ public TaskHubWorker AddTaskActivities(params ObjectCreator<TaskActivity>[] 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.
/// </summary>
/// <remarks>
/// This is deprecated and exists only for back-compatibility.
/// See <see cref="AddTaskActivitiesFromInterfaceV2"/>, which adds support for C# interface features such as inheritance, generics, and method overloading.
/// </remarks>
/// <typeparam name="T">Interface</typeparam>
/// <param name="activities">Object that implements this interface</param>
public TaskHubWorker AddTaskActivitiesFromInterface<T>(T activities)
Expand All @@ -452,6 +456,10 @@ public TaskHubWorker AddTaskActivitiesFromInterface<T>(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.
/// </summary>
/// <remarks>
/// This is deprecated and exists only for back-compatibility.
/// See <see cref="AddTaskActivitiesFromInterfaceV2"/>, which adds support for C# interface features such as inheritance, generics, and method overloading.
/// </remarks>
/// <typeparam name="T">Interface</typeparam>
/// <param name="activities">Object that implements this interface</param>
/// <param name="useFullyQualifiedMethodNames">
Expand All @@ -469,6 +477,23 @@ public TaskHubWorker AddTaskActivitiesFromInterface<T>(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.
/// </summary>
/// <typeparam name="T">Interface</typeparam>
/// <param name="activities">Object that implements this interface</param>
public TaskHubWorker AddTaskActivitiesFromInterfaceV2<T>(object activities)
{
return this.AddTaskActivitiesFromInterfaceV2(typeof(T), activities);
}

/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// This is deprecated and exists only for back-compatibility.
/// See <see cref="AddTaskActivitiesFromInterfaceV2"/>, which adds support for C# interface features such as inheritance, generics, and method overloading.
/// </remarks>
/// <param name="interface">Interface type.</param>
/// <param name="activities">Object that implements the <paramref name="interface"/> interface</param>
/// <param name="useFullyQualifiedMethodNames">
Expand All @@ -477,16 +502,7 @@ public TaskHubWorker AddTaskActivitiesFromInterface<T>(T activities, bool useFul
/// </param>
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);
Expand All @@ -500,6 +516,29 @@ public TaskHubWorker AddTaskActivitiesFromInterface(Type @interface, object acti
return this;
}

/// <summary>
/// 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.
/// </summary>
/// <param name="interface">Interface type.</param>
/// <param name="activities">Object that implements the <paramref name="interface"/> interface</param>
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<TaskActivity> creator = new NameValueObjectCreator<TaskActivity>(name, NameVersionHelper.GetDefaultVersion(methodInfo), taskActivity);
this.AddTaskActivities(creator);
}

return this;
}

/// <summary>
/// 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
Expand Down Expand Up @@ -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}");
}
}
}
}
Loading