Skip to content

Commit 764746a

Browse files
author
Connor McMahon
authored
Add IDurableEntityContext.DispatchAsync() directly to interface (#1581)
In a similar vein to PR #1422, this PR aims to transfer an extension method to a direct interface implementation. This allows the method to be directly mockable via tooling like Moq. Addresses #1419
1 parent 2b5e582 commit 764746a

File tree

6 files changed

+149
-172
lines changed

6 files changed

+149
-172
lines changed

src/WebJobs.Extensions.DurableTask/ContextImplementations/DurableEntityContext.cs

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
using System;
55
using System.Collections.Generic;
66
using System.Linq;
7+
using System.Reflection;
78
using System.Runtime.ExceptionServices;
9+
using System.Threading.Tasks;
810
using DurableTask.Core;
911
using Microsoft.Azure.WebJobs.Host.Bindings;
1012
using Newtonsoft.Json;
@@ -415,12 +417,95 @@ string IDurableEntityContext.StartNewOrchestration(string functionName, object i
415417
return instanceId;
416418
}
417419

420+
async Task IDurableEntityContext.DispatchAsync<T>(params object[] constructorParameters)
421+
{
422+
IDurableEntityContext context = (IDurableEntityContext)this;
423+
MethodInfo method = FindMethodForContext<T>(context);
424+
425+
if (method == null)
426+
{
427+
// We support a default delete operation even if the interface does not explicitly have a Delete method.
428+
if (string.Equals("delete", context.OperationName, StringComparison.InvariantCultureIgnoreCase))
429+
{
430+
Entity.Current.DeleteState();
431+
return;
432+
}
433+
else
434+
{
435+
throw new InvalidOperationException($"No operation named '{context.OperationName}' was found.");
436+
}
437+
}
438+
439+
// check that the number of arguments is zero or one
440+
ParameterInfo[] parameters = method.GetParameters();
441+
if (parameters.Length > 1)
442+
{
443+
throw new InvalidOperationException("Only a single argument can be used for operation input.");
444+
}
445+
446+
object[] args;
447+
if (parameters.Length == 1)
448+
{
449+
// determine the expected type of the operation input and deserialize
450+
Type inputType = method.GetParameters()[0].ParameterType;
451+
object input = context.GetInput(inputType);
452+
args = new object[1] { input };
453+
}
454+
else
455+
{
456+
args = Array.Empty<object>();
457+
}
458+
459+
#if !FUNCTIONS_V1
460+
T Constructor() => (T)context.FunctionBindingContext.CreateObjectInstance(typeof(T), constructorParameters);
461+
#else
462+
T Constructor() => (T)Activator.CreateInstance(typeof(T), constructorParameters);
463+
#endif
464+
465+
var state = ((Extensions.DurableTask.DurableEntityContext)context).GetStateWithInjectedDependencies(Constructor);
466+
467+
object result = method.Invoke(state, args);
468+
469+
if (method.ReturnType != typeof(void))
470+
{
471+
if (result is Task task)
472+
{
473+
await task;
474+
475+
if (task.GetType().IsGenericType)
476+
{
477+
context.Return(task.GetType().GetProperty("Result").GetValue(task));
478+
}
479+
}
480+
else
481+
{
482+
context.Return(result);
483+
}
484+
}
485+
}
486+
418487
void IDurableEntityContext.Return(object result)
419488
{
420489
this.ThrowIfInvalidAccess();
421490
this.CurrentOperationResponse.SetResult(result, this.messageDataConverter);
422491
}
423492

493+
internal static MethodInfo FindMethodForContext<T>(IDurableEntityContext context)
494+
{
495+
var type = typeof(T);
496+
497+
var interfaces = type.GetInterfaces();
498+
const BindingFlags bindingFlags = BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
499+
500+
var method = type.GetMethod(context.OperationName, bindingFlags);
501+
if (interfaces.Length == 0 || method != null)
502+
{
503+
return method;
504+
}
505+
506+
return interfaces.Select(i => i.GetMethod(context.OperationName, bindingFlags)).FirstOrDefault(m => m != null);
507+
}
508+
424509
private void ThrowIfInvalidAccess()
425510
{
426511
if (this.CurrentOperation == null)

src/WebJobs.Extensions.DurableTask/ContextInterfaces/IDurableEntityContext.cs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
// Licensed under the MIT License. See LICENSE in the project root for license information.
33

44
using System;
5+
using System.Reflection;
6+
using System.Threading.Tasks;
57
using Microsoft.Azure.WebJobs.Host.Bindings;
68

79
namespace Microsoft.Azure.WebJobs.Extensions.DurableTask
@@ -169,5 +171,25 @@ public interface IDurableEntityContext
169171
/// </exception>
170172
/// <returns>The instance id of the new orchestration.</returns>
171173
string StartNewOrchestration(string functionName, object input, string instanceId = null);
174+
175+
/// <summary>
176+
/// Dynamically dispatches the incoming entity operation using reflection.
177+
/// </summary>
178+
/// <typeparam name="T">The class to use for entity instances.</typeparam>
179+
/// <returns>A task that completes when the dispatched operation has finished.</returns>
180+
/// <exception cref="AmbiguousMatchException">If there is more than one method with the given operation name.</exception>
181+
/// <exception cref="MissingMethodException">If there is no method with the given operation name.</exception>
182+
/// <exception cref="InvalidOperationException">If the method has more than one argument.</exception>
183+
/// <remarks>
184+
/// If the entity's state is null, an object of type <typeparamref name="T"/> is created first. Then, reflection
185+
/// is used to try to find a matching method. This match is based on the method name
186+
/// (which is the operation name) and the argument list (which is the operation content, deserialized into
187+
/// an object array).
188+
/// </remarks>
189+
/// <param name="constructorParameters">Parameters to feed to the entity constructor. Should be primarily used for
190+
/// output bindings. Parameters must match the order in the constructor after ignoring parameters populated on
191+
/// constructor via dependency injection.</param>
192+
Task DispatchAsync<T>(params object[] constructorParameters)
193+
where T : class;
172194
}
173195
}

src/WebJobs.Extensions.DurableTask/EntityScheduler/TypedInvocationExtensions.cs

Lines changed: 0 additions & 116 deletions
This file was deleted.

src/WebJobs.Extensions.DurableTask/Microsoft.Azure.WebJobs.Extensions.DurableTask-net461.xml

Lines changed: 19 additions & 25 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/WebJobs.Extensions.DurableTask/Microsoft.Azure.WebJobs.Extensions.DurableTask.xml

Lines changed: 19 additions & 25 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)