src/WebJobs.Extensions.DurableTask/EntityScheduler/Proxy/EntityProxyFactory.cs (129 lines of code) (raw):
// 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.Collections.Concurrent;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Threading.Tasks;
namespace Microsoft.Azure.WebJobs.Extensions.DurableTask
{
internal static class EntityProxyFactory
{
private static readonly ModuleBuilder DynamicModuleBuilder;
private static readonly ConcurrentDictionary<Type, Type> TypeMappings = new ConcurrentDictionary<Type, Type>();
static EntityProxyFactory()
{
var assemblyName = new AssemblyName($"DynamicAssembly_{Guid.NewGuid():N}");
var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
DynamicModuleBuilder = assemblyBuilder.DefineDynamicModule("DynamicModule");
}
internal static TEntityInterface Create<TEntityInterface>(IEntityProxyContext context, EntityId entityId)
{
var type = TypeMappings.GetOrAdd(typeof(TEntityInterface), CreateProxyType);
return (TEntityInterface)Activator.CreateInstance(type, context, entityId);
}
private static Type CreateProxyType(Type interfaceType)
{
ValidateInterface(interfaceType);
var typeName = $"{interfaceType.Name}_{Guid.NewGuid():N}";
var typeBuilder = DynamicModuleBuilder.DefineType(
typeName,
TypeAttributes.Public | TypeAttributes.BeforeFieldInit | TypeAttributes.AnsiClass,
typeof(EntityProxy));
typeBuilder.AddInterfaceImplementation(interfaceType);
BuildConstructor(typeBuilder);
BuildMethods(typeBuilder, interfaceType);
return typeBuilder.CreateTypeInfo();
}
private static void ValidateInterface(Type interfaceType)
{
if (!interfaceType.IsInterface)
{
throw new InvalidOperationException($"{interfaceType.Name} is not an interface. Entity proxy type parameters must be interfaces.");
}
if (interfaceType.GetProperties(BindingFlags.Instance | BindingFlags.Public).Length > 0)
{
throw new InvalidOperationException($"Interface '{interfaceType.FullName}' defines properties. Entity proxy interfaces with properties are not supported.");
}
}
private static void BuildConstructor(TypeBuilder typeBuilder)
{
var ctorArgTypes = new[] { typeof(IEntityProxyContext), typeof(EntityId) };
// Create ctor
var ctor = typeBuilder.DefineConstructor(
MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName,
CallingConventions.Standard,
ctorArgTypes);
var ilGenerator = ctor.GetILGenerator();
ilGenerator.Emit(OpCodes.Ldarg_0);
ilGenerator.Emit(OpCodes.Ldarg_1);
ilGenerator.Emit(OpCodes.Ldarg_2);
ilGenerator.Emit(OpCodes.Call, typeof(EntityProxy).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, ctorArgTypes, null));
ilGenerator.Emit(OpCodes.Ret);
}
private static void BuildMethods(TypeBuilder typeBuilder, Type interfaceType)
{
var methods = interfaceType.GetMethods(BindingFlags.Instance | BindingFlags.Public).ToList();
// Interfaces can inherit from other interfaces, getting those methods too
var interfaces = interfaceType.GetInterfaces();
if (interfaces.Length > 0)
{
foreach (var inter in interfaces)
{
methods.AddRange(inter.GetMethods(BindingFlags.Instance | BindingFlags.Public));
}
}
if (methods.Count == 0)
{
throw new InvalidOperationException($"Interface '{interfaceType.FullName}' has no methods defined.");
}
var entityProxyMethods = typeof(EntityProxy).GetMethods(BindingFlags.NonPublic | BindingFlags.Instance);
var callAsyncMethod = entityProxyMethods.First(x => x.Name == nameof(EntityProxy.CallAsync) && !x.IsGenericMethod);
var callAsyncGenericMethod = entityProxyMethods.First(x => x.Name == nameof(EntityProxy.CallAsync) && x.IsGenericMethod);
var signalMethod = entityProxyMethods.First(x => x.Name == nameof(EntityProxy.Signal));
foreach (var methodInfo in methods)
{
var parameters = methodInfo.GetParameters();
// check that the number of arguments is zero or one
if (parameters.Length > 1)
{
throw new InvalidOperationException($"Method '{methodInfo.Name}' defines more than one parameter. Entity proxy interface methods must define at most one argument for operation input.");
}
var returnType = methodInfo.ReturnType;
// check that return type is void / Task / Task<T>.
if (returnType != typeof(void) && !(returnType == typeof(Task) || returnType.BaseType == typeof(Task)))
{
throw new InvalidOperationException($"Method '{methodInfo.Name}' has a return type which is neither void nor a Task. Entity proxy interface methods may only return void, Task, or Task<T>.");
}
var proxyMethod = typeBuilder.DefineMethod(
methodInfo.Name,
MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.NewSlot | MethodAttributes.SpecialName | MethodAttributes.Virtual,
returnType,
parameters.Length == 0 ? null : new[] { parameters[0].ParameterType });
typeBuilder.DefineMethodOverride(proxyMethod, methodInfo);
var ilGenerator = proxyMethod.GetILGenerator();
ilGenerator.Emit(OpCodes.Ldarg_0);
ilGenerator.Emit(OpCodes.Ldstr, methodInfo.Name);
if (parameters.Length == 0)
{
ilGenerator.Emit(OpCodes.Ldnull);
}
else
{
ilGenerator.Emit(OpCodes.Ldarg_1);
// ValueType needs boxing.
if (parameters[0].ParameterType.IsValueType)
{
ilGenerator.Emit(OpCodes.Box, parameters[0].ParameterType);
}
}
if (returnType == typeof(void))
{
ilGenerator.Emit(OpCodes.Call, signalMethod);
}
else
{
ilGenerator.DeclareLocal(returnType);
ilGenerator.Emit(OpCodes.Call, returnType.IsGenericType ? callAsyncGenericMethod.MakeGenericMethod(returnType.GetGenericArguments()[0]) : callAsyncMethod);
ilGenerator.Emit(OpCodes.Stloc_0);
ilGenerator.Emit(OpCodes.Ldloc_0);
}
ilGenerator.Emit(OpCodes.Ret);
}
}
}
}