/// <summary> /// Constructs a query/signal method map for a workflow type. /// </summary> /// <param name="workflowType">The workflow type.</param> /// <returns>The <see cref="WorkflowMethodMap"/>.</returns> public static WorkflowMethodMap Create(Type workflowType) { Covenant.Requires <ArgumentNullException>(workflowType != null, nameof(workflowType)); Covenant.Requires <ArgumentException>(!workflowType.IsInterface, nameof(workflowType)); var map = new WorkflowMethodMap(); foreach (var @interface in workflowType.GetInterfaces()) { foreach (var method in @interface.GetMethods(BindingFlags.Public | BindingFlags.Instance)) { // Signal methods are tagged by [SignalMethod]. var signalMethodAttribute = method.GetCustomAttribute <SignalMethodAttribute>(); if (signalMethodAttribute != null) { map.nameToSignalMethod[signalMethodAttribute.Name] = method; continue; } // Query methods are tagged by [QueryMethod]. var queryMethodAttribute = method.GetCustomAttribute <QueryMethodAttribute>(); if (queryMethodAttribute != null) { map.nameToQueryMethod[queryMethodAttribute.Name] = method; continue; } } } return(map); }
/// <summary> /// Registers a workflow implementation. /// </summary> /// <param name="client">The associated client.</param> /// <param name="workflowType">The workflow implementation type.</param> /// <param name="workflowTypeName">The name used to identify the implementation.</param> /// <returns><c>true</c> if the workflow was already registered.</returns> /// <exception cref="InvalidOperationException">Thrown if a different workflow class has already been registered for <paramref name="workflowTypeName"/>.</exception> internal static bool Register(CadenceClient client, Type workflowType, string workflowTypeName) { Covenant.Requires <ArgumentNullException>(client != null); CadenceHelper.ValidateWorkflowImplementation(workflowType); // We need to register each workflow method defined for the workflow. var methodMap = WorkflowMethodMap.Create(workflowType); foreach (var method in workflowType.GetMethods()) { var workflowMethodAttribute = method.GetCustomAttribute <WorkflowMethodAttribute>(); if (workflowMethodAttribute == null) { continue; } workflowTypeName = GetWorkflowTypeKey(client, workflowTypeName, method); lock (syncLock) { if (nameToRegistration.TryGetValue(workflowTypeName, out var existingRegistration)) { if (!object.ReferenceEquals(existingRegistration.WorkflowType, workflowType)) { throw new InvalidOperationException($"Conflicting workflow interface registration: Workflow interface [{workflowType.FullName}] is already registered for workflow type name [{workflowTypeName}]."); } return(true); } else { nameToRegistration[workflowTypeName] = new WorkflowRegistration() { WorkflowType = workflowType, WorkflowMethod = method, MethodMap = methodMap }; } } } return(false); }
/// <summary> /// Constructs a query/signal method map for a workflow type. /// </summary> /// <param name="workflowType">The workflow interface.</param> /// <returns>The <see cref="WorkflowMethodMap"/>.</returns> public static WorkflowMethodMap Create(Type workflowType) { Covenant.Requires <ArgumentNullException>(workflowType != null); // $todo(jeff.lill): // // The code below doesn't not verify that query/signal names are unique // but also doesn't barf. It will send requets to the last method // encountered with the same name, which is pretty reasonable. // // In a perfect world, we'd detect this and throw an exception. var map = new WorkflowMethodMap(); foreach (var method in workflowType.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)) { // Signal methods are tagged by [SignalHandler], accept a single byte array parameter, // and returns [Task]. var signalHandlerAttribute = method.GetCustomAttribute <SignalMethodAttribute>(); if (signalHandlerAttribute != null) { if (method.ReturnType != typeof(Task)) { Log.LogWarn($"Workflow [{workflowType.FullName}.{method.Name}()] signal handler is invalid because it doesn't return [void]. It will be ignored."); continue; } var parameters = method.GetParameters(); if (parameters.Length != 1 || parameters[0].ParameterType != typeof(byte[])) { Log.LogWarn($"Workflow [{workflowType.FullName}.{method.Name}()] signal handler is invalid because it doesn't accept a single byte array parameter. It will be ignored."); continue; } map.nameToSignalMethod[signalHandlerAttribute.Name] = method; continue; } // Query methods are tagged by [QueryHandler], accept a single byte array parameter, // and returns [Task<byte[]>]. var queryHandlerAttribute = method.GetCustomAttribute <QueryMethodAttribute>(); if (queryHandlerAttribute != null) { if (method.ReturnType != typeof(Task <byte[]>)) { Log.LogWarn($"Workflow [{workflowType.FullName}.{method.Name}()] query handler is invalid because it doesn't return a byte array. It will be ignored."); continue; } var parameters = method.GetParameters(); if (parameters.Length != 1 || parameters[0].ParameterType != typeof(byte[])) { Log.LogWarn($"Workflow [{workflowType.FullName}.{method.Name}()] query handler is invalid because it doesn't accept a single byte array parameter. It will be ignored."); continue; } map.nameToQueryMethod[queryHandlerAttribute.Name] = method; continue; } } return(map); }
/// <summary> /// Registers a workflow implementation. /// </summary> /// <param name="client">The associated client.</param> /// <param name="workflowType">The workflow implementation type.</param> /// <param name="workflowTypeName">The name used to identify the implementation.</param> /// <param name="domain">Specifies the target domain.</param> /// <exception cref="InvalidOperationException">Thrown if a different workflow class has already been registered for <paramref name="workflowTypeName"/>.</exception> internal static async Task RegisterAsync(CadenceClient client, Type workflowType, string workflowTypeName, string domain) { Covenant.Requires<ArgumentNullException>(client != null, nameof(client)); Covenant.Requires<ArgumentNullException>(!string.IsNullOrEmpty(domain), nameof(domain)); CadenceHelper.ValidateWorkflowImplementation(workflowType); var methodMap = WorkflowMethodMap.Create(workflowType); // We need to register each workflow method that implements a workflow interface method // with the same signature that that was tagged by [WorkflowMethod]. // // First, we'll create a dictionary that maps method signatures from any inherited // interfaces that are tagged by [WorkflowMethod] to the attribute. var methodSignatureToAttribute = new Dictionary<string, WorkflowMethodAttribute>(); foreach (var interfaceType in workflowType.GetInterfaces()) { foreach (var method in interfaceType.GetMethods(BindingFlags.Public | BindingFlags.Instance)) { var workflowMethodAttribute = method.GetCustomAttribute<WorkflowMethodAttribute>(); if (workflowMethodAttribute == null) { continue; } var signature = method.ToString(); if (methodSignatureToAttribute.ContainsKey(signature)) { throw new NotSupportedException($"Workflow type [{workflowType.FullName}] cannot implement the [{signature}] method from two different interfaces."); } methodSignatureToAttribute.Add(signature, workflowMethodAttribute); } } // Next, we need to register the workflow methods that implement the // workflow interface. foreach (var method in workflowType.GetMethods()) { if (!methodSignatureToAttribute.TryGetValue(method.ToString(), out var workflowMethodAttribute)) { continue; } var workflowTypeKey = GetWorkflowTypeKey(client, workflowTypeName, workflowMethodAttribute); lock (syncLock) { if (nameToRegistration.TryGetValue(workflowTypeName, out var existingRegistration)) { if (!object.ReferenceEquals(existingRegistration.WorkflowType, workflowType)) { throw new InvalidOperationException($"Conflicting workflow interface registration: Workflow interface [{workflowType.FullName}] is already registered for workflow type name [{workflowTypeName}]."); } } else { nameToRegistration[workflowTypeKey] = new WorkflowRegistration() { WorkflowType = workflowType, WorkflowMethod = method, WorkflowMethodParameterTypes = method.GetParameterTypes(), MethodMap = methodMap }; } } var reply = (WorkflowRegisterReply)await client.CallProxyAsync( new WorkflowRegisterRequest() { Name = GetWorkflowTypeNameFromKey(workflowTypeKey), Domain = client.ResolveDomain(domain) }); // $hack(jefflill): // // We're going to ignore any errors here to handle: // // https://github.com/nforgeio/neonKUBE/issues/668 // reply.ThrowOnError(); } }