Skip to content

Commit

Permalink
Add IDurableEntityContext.DispatchAsync() directly to interface (#1581)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
Connor McMahon committed Nov 25, 2020
1 parent 2b5e582 commit 764746a
Show file tree
Hide file tree
Showing 6 changed files with 149 additions and 172 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -415,12 +417,95 @@ string IDurableEntityContext.StartNewOrchestration(string functionName, object i
return instanceId;
}

async Task IDurableEntityContext.DispatchAsync<T>(params object[] constructorParameters)
{
IDurableEntityContext context = (IDurableEntityContext)this;
MethodInfo method = FindMethodForContext<T>(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<object>();
}

#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<T>(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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -169,5 +171,25 @@ public interface IDurableEntityContext
/// </exception>
/// <returns>The instance id of the new orchestration.</returns>
string StartNewOrchestration(string functionName, object input, string instanceId = null);

/// <summary>
/// Dynamically dispatches the incoming entity operation using reflection.
/// </summary>
/// <typeparam name="T">The class to use for entity instances.</typeparam>
/// <returns>A task that completes when the dispatched operation has finished.</returns>
/// <exception cref="AmbiguousMatchException">If there is more than one method with the given operation name.</exception>
/// <exception cref="MissingMethodException">If there is no method with the given operation name.</exception>
/// <exception cref="InvalidOperationException">If the method has more than one argument.</exception>
/// <remarks>
/// If the entity's state is null, an object of type <typeparamref name="T"/> 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).
/// </remarks>
/// <param name="constructorParameters">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.</param>
Task DispatchAsync<T>(params object[] constructorParameters)
where T : class;
}
}

This file was deleted.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 764746a

Please sign in to comment.