Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add IDurableEntityContext.DispatchAsync() directly to interface #1581

Merged
merged 2 commits into from
Nov 25, 2020
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
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)
ConnorMcMahon marked this conversation as resolved.
Show resolved Hide resolved
{
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);
ConnorMcMahon marked this conversation as resolved.
Show resolved Hide resolved
}

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.
ConnorMcMahon marked this conversation as resolved.
Show resolved Hide resolved
/// </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