diff --git a/src/WebJobs.Extensions.DurableTask/ContextImplementations/DurableEntityContext.cs b/src/WebJobs.Extensions.DurableTask/ContextImplementations/DurableEntityContext.cs index 56ca35d3e..27dfc7ee4 100644 --- a/src/WebJobs.Extensions.DurableTask/ContextImplementations/DurableEntityContext.cs +++ b/src/WebJobs.Extensions.DurableTask/ContextImplementations/DurableEntityContext.cs @@ -4,7 +4,9 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Reflection; using System.Runtime.ExceptionServices; +using System.Threading.Tasks; using DurableTask.Core; using Microsoft.Azure.WebJobs.Host.Bindings; using Newtonsoft.Json; @@ -415,12 +417,95 @@ string IDurableEntityContext.StartNewOrchestration(string functionName, object i return instanceId; } + async Task IDurableEntityContext.DispatchAsync(params object[] constructorParameters) + { + IDurableEntityContext context = (IDurableEntityContext)this; + MethodInfo method = FindMethodForContext(context); + + if (method == null) + { + // We support a default delete operation even if the interface does not explicitly have a Delete method. + if (string.Equals("delete", context.OperationName, StringComparison.InvariantCultureIgnoreCase)) + { + Entity.Current.DeleteState(); + return; + } + else + { + throw new InvalidOperationException($"No operation named '{context.OperationName}' was found."); + } + } + + // check that the number of arguments is zero or one + ParameterInfo[] parameters = method.GetParameters(); + if (parameters.Length > 1) + { + throw new InvalidOperationException("Only a single argument can be used for operation input."); + } + + object[] args; + if (parameters.Length == 1) + { + // determine the expected type of the operation input and deserialize + Type inputType = method.GetParameters()[0].ParameterType; + object input = context.GetInput(inputType); + args = new object[1] { input }; + } + else + { + args = Array.Empty(); + } + +#if !FUNCTIONS_V1 + T Constructor() => (T)context.FunctionBindingContext.CreateObjectInstance(typeof(T), constructorParameters); +#else + T Constructor() => (T)Activator.CreateInstance(typeof(T), constructorParameters); +#endif + + var state = ((Extensions.DurableTask.DurableEntityContext)context).GetStateWithInjectedDependencies(Constructor); + + object result = method.Invoke(state, args); + + if (method.ReturnType != typeof(void)) + { + if (result is Task task) + { + await task; + + if (task.GetType().IsGenericType) + { + context.Return(task.GetType().GetProperty("Result").GetValue(task)); + } + } + else + { + context.Return(result); + } + } + } + void IDurableEntityContext.Return(object result) { this.ThrowIfInvalidAccess(); this.CurrentOperationResponse.SetResult(result, this.messageDataConverter); } + internal static MethodInfo FindMethodForContext(IDurableEntityContext context) + { + var type = typeof(T); + + var interfaces = type.GetInterfaces(); + const BindingFlags bindingFlags = BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; + + var method = type.GetMethod(context.OperationName, bindingFlags); + if (interfaces.Length == 0 || method != null) + { + return method; + } + + return interfaces.Select(i => i.GetMethod(context.OperationName, bindingFlags)).FirstOrDefault(m => m != null); + } + private void ThrowIfInvalidAccess() { if (this.CurrentOperation == null) diff --git a/src/WebJobs.Extensions.DurableTask/ContextInterfaces/IDurableEntityContext.cs b/src/WebJobs.Extensions.DurableTask/ContextInterfaces/IDurableEntityContext.cs index ed32ffc47..b2d68d809 100644 --- a/src/WebJobs.Extensions.DurableTask/ContextInterfaces/IDurableEntityContext.cs +++ b/src/WebJobs.Extensions.DurableTask/ContextInterfaces/IDurableEntityContext.cs @@ -2,6 +2,8 @@ // Licensed under the MIT License. See LICENSE in the project root for license information. using System; +using System.Reflection; +using System.Threading.Tasks; using Microsoft.Azure.WebJobs.Host.Bindings; namespace Microsoft.Azure.WebJobs.Extensions.DurableTask @@ -169,5 +171,25 @@ public interface IDurableEntityContext /// /// The instance id of the new orchestration. string StartNewOrchestration(string functionName, object input, string instanceId = null); + + /// + /// Dynamically dispatches the incoming entity operation using reflection. + /// + /// The class to use for entity instances. + /// A task that completes when the dispatched operation has finished. + /// If there is more than one method with the given operation name. + /// If there is no method with the given operation name. + /// If the method has more than one argument. + /// + /// If the entity's state is null, an object of type is created first. Then, reflection + /// is used to try to find a matching method. This match is based on the method name + /// (which is the operation name) and the argument list (which is the operation content, deserialized into + /// an object array). + /// + /// Parameters to feed to the entity constructor. Should be primarily used for + /// output bindings. Parameters must match the order in the constructor after ignoring parameters populated on + /// constructor via dependency injection. + Task DispatchAsync(params object[] constructorParameters) + where T : class; } } \ No newline at end of file diff --git a/src/WebJobs.Extensions.DurableTask/EntityScheduler/TypedInvocationExtensions.cs b/src/WebJobs.Extensions.DurableTask/EntityScheduler/TypedInvocationExtensions.cs deleted file mode 100644 index 57e558746..000000000 --- a/src/WebJobs.Extensions.DurableTask/EntityScheduler/TypedInvocationExtensions.cs +++ /dev/null @@ -1,116 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT License. See LICENSE in the project root for license information. - -using System; -using System.Linq; -using System.Reflection; -using System.Threading.Tasks; - -namespace Microsoft.Azure.WebJobs.Extensions.DurableTask -{ - /// - /// Extends the durable entity context to support reflection-based invocation of entity operations. - /// - public static class TypedInvocationExtensions - { - /// - /// Dynamically dispatches the incoming entity operation using reflection. - /// - /// The class to use for entity instances. - /// A task that completes when the dispatched operation has finished. - /// If there is more than one method with the given operation name. - /// If there is no method with the given operation name. - /// If the method has more than one argument. - /// - /// If the entity's state is null, an object of type is created first. Then, reflection - /// is used to try to find a matching method. This match is based on the method name - /// (which is the operation name) and the argument list (which is the operation content, deserialized into - /// an object array). - /// - /// Context object to use to dispatch entity operations. - /// Parameters to feed to the entity constructor. Should be primarily used for - /// output bindings. Parameters must match the order in the constructor after ignoring parameters populated on - /// constructor via dependency injection. - public static async Task DispatchAsync(this IDurableEntityContext context, params object[] constructorParameters) - where T : class - { - MethodInfo method = FindMethodForContext(context); - - if (method == null) - { - if (string.Equals("delete", context.OperationName, StringComparison.InvariantCultureIgnoreCase)) - { - Entity.Current.DeleteState(); - return; - } - else - { - throw new InvalidOperationException($"No operation named '{context.OperationName}' was found."); - } - } - - // check that the number of arguments is zero or one - ParameterInfo[] parameters = method.GetParameters(); - if (parameters.Length > 1) - { - throw new InvalidOperationException("Only a single argument can be used for operation input."); - } - - object[] args; - if (parameters.Length == 1) - { - // determine the expected type of the operation input and deserialize - Type inputType = method.GetParameters()[0].ParameterType; - object input = context.GetInput(inputType); - args = new object[1] { input }; - } - else - { - args = Array.Empty(); - } - -#if !FUNCTIONS_V1 - T Constructor() => (T)context.FunctionBindingContext.CreateObjectInstance(typeof(T), constructorParameters); -#else - T Constructor() => (T)Activator.CreateInstance(typeof(T), constructorParameters); -#endif - - var state = ((Extensions.DurableTask.DurableEntityContext)context).GetStateWithInjectedDependencies(Constructor); - - object result = method.Invoke(state, args); - - if (method.ReturnType != typeof(void)) - { - if (result is Task task) - { - await task; - - if (task.GetType().IsGenericType) - { - context.Return(task.GetType().GetProperty("Result").GetValue(task)); - } - } - else - { - context.Return(result); - } - } - } - - internal static MethodInfo FindMethodForContext(IDurableEntityContext context) - { - var type = typeof(T); - - var interfaces = type.GetInterfaces(); - const BindingFlags bindingFlags = BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; - - var method = type.GetMethod(context.OperationName, bindingFlags); - if (interfaces.Length == 0 || method != null) - { - return method; - } - - return interfaces.Select(i => i.GetMethod(context.OperationName, bindingFlags)).FirstOrDefault(m => m != null); - } - } -} diff --git a/src/WebJobs.Extensions.DurableTask/Microsoft.Azure.WebJobs.Extensions.DurableTask-net461.xml b/src/WebJobs.Extensions.DurableTask/Microsoft.Azure.WebJobs.Extensions.DurableTask-net461.xml index d293e7923..e78a7a8f6 100644 --- a/src/WebJobs.Extensions.DurableTask/Microsoft.Azure.WebJobs.Extensions.DurableTask-net461.xml +++ b/src/WebJobs.Extensions.DurableTask/Microsoft.Azure.WebJobs.Extensions.DurableTask-net461.xml @@ -705,6 +705,25 @@ The instance id of the new orchestration. + + + Dynamically dispatches the incoming entity operation using reflection. + + The class to use for entity instances. + A task that completes when the dispatched operation has finished. + If there is more than one method with the given operation name. + If there is no method with the given operation name. + If the method has more than one argument. + + If the entity's state is null, an object of type is created first. Then, reflection + is used to try to find a matching method. This match is based on the method name + (which is the operation name) and the argument list (which is the operation content, deserialized into + an object array). + + Parameters to feed to the entity constructor. Should be primarily used for + output bindings. Parameters must match the order in the constructor after ignoring parameters populated on + constructor via dependency injection. + Provides functionality available to durable orchestration clients. @@ -2628,31 +2647,6 @@ The metadata used for reordering and deduplication of messages sent to entities. - - - Extends the durable entity context to support reflection-based invocation of entity operations. - - - - - Dynamically dispatches the incoming entity operation using reflection. - - The class to use for entity instances. - A task that completes when the dispatched operation has finished. - If there is more than one method with the given operation name. - If there is no method with the given operation name. - If the method has more than one argument. - - If the entity's state is null, an object of type is created first. Then, reflection - is used to try to find a matching method. This match is based on the method name - (which is the operation name) and the argument list (which is the operation content, deserialized into - an object array). - - Context object to use to dispatch entity operations. - Parameters to feed to the entity constructor. Should be primarily used for - output bindings. Parameters must match the order in the constructor after ignoring parameters populated on - constructor via dependency injection. - ETW Event Provider for the WebJobs.Extensions.DurableTask extension. diff --git a/src/WebJobs.Extensions.DurableTask/Microsoft.Azure.WebJobs.Extensions.DurableTask.xml b/src/WebJobs.Extensions.DurableTask/Microsoft.Azure.WebJobs.Extensions.DurableTask.xml index b0a447b15..56ed88054 100644 --- a/src/WebJobs.Extensions.DurableTask/Microsoft.Azure.WebJobs.Extensions.DurableTask.xml +++ b/src/WebJobs.Extensions.DurableTask/Microsoft.Azure.WebJobs.Extensions.DurableTask.xml @@ -710,6 +710,25 @@ The instance id of the new orchestration. + + + Dynamically dispatches the incoming entity operation using reflection. + + The class to use for entity instances. + A task that completes when the dispatched operation has finished. + If there is more than one method with the given operation name. + If there is no method with the given operation name. + If the method has more than one argument. + + If the entity's state is null, an object of type is created first. Then, reflection + is used to try to find a matching method. This match is based on the method name + (which is the operation name) and the argument list (which is the operation content, deserialized into + an object array). + + Parameters to feed to the entity constructor. Should be primarily used for + output bindings. Parameters must match the order in the constructor after ignoring parameters populated on + constructor via dependency injection. + Provides functionality available to durable orchestration clients. @@ -2856,31 +2875,6 @@ The metadata used for reordering and deduplication of messages sent to entities. - - - Extends the durable entity context to support reflection-based invocation of entity operations. - - - - - Dynamically dispatches the incoming entity operation using reflection. - - The class to use for entity instances. - A task that completes when the dispatched operation has finished. - If there is more than one method with the given operation name. - If there is no method with the given operation name. - If the method has more than one argument. - - If the entity's state is null, an object of type is created first. Then, reflection - is used to try to find a matching method. This match is based on the method name - (which is the operation name) and the argument list (which is the operation content, deserialized into - an object array). - - Context object to use to dispatch entity operations. - Parameters to feed to the entity constructor. Should be primarily used for - output bindings. Parameters must match the order in the constructor after ignoring parameters populated on - constructor via dependency injection. - ETW Event Provider for the WebJobs.Extensions.DurableTask extension. diff --git a/test/FunctionsV2/EntityMethodDiscoveryTests.cs b/test/FunctionsV2/EntityMethodDiscoveryTests.cs index 581bf7eab..3282a0b10 100644 --- a/test/FunctionsV2/EntityMethodDiscoveryTests.cs +++ b/test/FunctionsV2/EntityMethodDiscoveryTests.cs @@ -1,8 +1,6 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. -using Microsoft.Azure.WebJobs.Extensions.DurableTask; -using Microsoft.Azure.WebJobs.Extensions.DurableTask.Tests; using Moq; using Xunit; @@ -22,7 +20,7 @@ public void CanFindMemberOnClassWithoutInterface() var context = new Mock(); context.Setup(ctx => ctx.OperationName).Returns("Method"); - var method = TypedInvocationExtensions.FindMethodForContext(context.Object); + var method = DurableEntityContext.FindMethodForContext(context.Object); Assert.NotNull(method); } @@ -34,7 +32,7 @@ public void WillReceiveNullForMissingMember() var context = new Mock(); context.Setup(ctx => ctx.OperationName).Returns("NonExistingMethod"); - var method = TypedInvocationExtensions.FindMethodForContext(context.Object); + var method = DurableEntityContext.FindMethodForContext(context.Object); Assert.Null(method); } @@ -46,7 +44,7 @@ public void WillFindMemberOnClassWithImplicitInterface() var context = new Mock(); context.Setup(ctx => ctx.OperationName).Returns("Add"); - var method = TypedInvocationExtensions.FindMethodForContext(context.Object); + var method = DurableEntityContext.FindMethodForContext(context.Object); Assert.NotNull(method); } @@ -58,7 +56,7 @@ public void WillFindMemberOnClassWithExplicitInterface() var context = new Mock(); context.Setup(ctx => ctx.OperationName).Returns("Add"); - var method = TypedInvocationExtensions.FindMethodForContext(context.Object); + var method = DurableEntityContext.FindMethodForContext(context.Object); Assert.NotNull(method); }