public async Task MethodCallAsync()

in src/Azure.IIoT.OpcUa.Publisher/src/Services/NodeServices.cs [603:807]


        public async Task<MethodCallResponseModel> MethodCallAsync(T endpoint,
            MethodCallRequestModel request, CancellationToken ct)
        {
            ArgumentNullException.ThrowIfNull(request);
            if (string.IsNullOrEmpty(request.ObjectId) &&
                (request.ObjectBrowsePath == null || request.ObjectBrowsePath.Count == 0))
            {
                throw new ArgumentException("Object id missing or bad browse path", nameof(request));
            }
            using var trace = _activitySource.StartActivity("MethodCall");
            return await _client.ExecuteAsync(endpoint, async context =>
            {
                //
                // A method call request can specify the targets in several ways:
                //
                // * Specify methodId and optionally objectId node ids with null browse paths.
                // * Specify an objectBrowsePath to a real object node from the node specified
                //   with objectId.  If objectId is null, the root node is used.
                // * Specify a methodBrowsePath from the above object node to the actual
                //   method node to call on the object. MethodId remains null.
                // * Like previously, but specify methodId and method browse path from it to a
                //   real method node.
                //
                var objectId = await context.Session.ResolveNodeIdAsync(request.Header, request.ObjectId,
                    request.ObjectBrowsePath, nameof(request.ObjectBrowsePath), _timeProvider,
                    context.Ct).ConfigureAwait(false);
                if (NodeId.IsNull(objectId))
                {
                    throw new ArgumentException("Object id missing", nameof(request));
                }

                var methodId = request.MethodId.ToNodeId(context.Session.MessageContext);
                if (request.MethodBrowsePath?.Count > 0)
                {
                    if (NodeId.IsNull(methodId))
                    {
                        // Browse from object id to method if possible
                        methodId = objectId ??
                            throw new ArgumentException("Method id and object id missing",
                                nameof(request));
                    }
                    methodId = await context.Session.ResolveBrowsePathToNodeAsync(request.Header,
                        methodId, [.. request.MethodBrowsePath], nameof(request.MethodBrowsePath),
                        _timeProvider, context.Ct).ConfigureAwait(false);
                }
                else if (NodeId.IsNull(methodId))
                {
                    // Method is null and cannot browse to method from object
                    throw new ArgumentException("Method id missing", nameof(request));
                }

                var browseDescriptions = new BrowseDescriptionCollection {
                    new BrowseDescription {
                        BrowseDirection = Opc.Ua.BrowseDirection.Forward,
                        IncludeSubtypes = true,
                        NodeClassMask = 0,
                        NodeId = methodId,
                        ReferenceTypeId = ReferenceTypeIds.HasProperty,
                        ResultMask = (uint)BrowseResultMask.All
                    }
                };
                // Get default input arguments and types
                var browse = await context.Session.Services.BrowseAsync(
                    request.Header.ToRequestHeader(_timeProvider), null, 0, browseDescriptions,
                    context.Ct).ConfigureAwait(false);

                var browseresults = browse.Validate(browse.Results, r => r.StatusCode,
                    browse.DiagnosticInfos, browseDescriptions);
                List<(TypeInfo, object)>? inputs = null, outputs = null;
                if (browseresults.ErrorInfo == null)
                {
                    foreach (var nodeReference in browseresults[0].Result.References)
                    {
                        List<(TypeInfo, object)>? temp = null;
                        if (nodeReference.BrowseName == BrowseNames.InputArguments)
                        {
                            temp = inputs = [];
                        }
                        else if (nodeReference.BrowseName == BrowseNames.OutputArguments)
                        {
                            temp = outputs = [];
                        }
                        else
                        {
                            continue;
                        }
                        var node = nodeReference.NodeId.ToNodeId(context.Session.MessageContext.NamespaceUris);
                        var (value, _) = await context.Session.ReadValueAsync(
                            request.Header.ToRequestHeader(_timeProvider),
                            node, context.Ct).ConfigureAwait(false);
                        // value is also null if the type is not a variable node
                        if (value?.Value is ExtensionObject[] argumentsList)
                        {
                            foreach (var argument in argumentsList.Select(a => (Argument)a.Body))
                            {
                                var builtInType = TypeInfo.GetBuiltInType(argument.DataType);
                                temp.Add((new TypeInfo(builtInType,
                                    argument.ValueRank), argument.Value));
                            }
                            if (inputs != null && outputs != null)
                            {
                                break;
                            }
                        }
                    }
                }

                if ((request.Arguments?.Count ?? 0) > (inputs?.Count ?? 0))
                {
                    // Too many arguments provided
                    if (browseresults.ErrorInfo != null)
                    {
                        return new MethodCallResponseModel
                        {
                            Results = Array.Empty<MethodCallArgumentModel>(),
                            ErrorInfo = browseresults.ErrorInfo
                        };
                    }
                    throw new ArgumentException("Arguments missing", nameof(request));
                }

                // Set default input arguments from meta data
                var requests = new CallMethodRequestCollection {
                    new CallMethodRequest {
                        ObjectId = objectId,
                        MethodId = methodId,
                        InputArguments = inputs == null ? [] :
                            new VariantCollection(inputs
                                .Select(arg => arg.Item1.CreateVariant(arg.Item2)))
                    }
                };

                // Update with input arguments provided in request payload
                if ((request.Arguments?.Count ?? 0) != 0)
                {
                    Debug.Assert(request.Arguments != null);
                    Debug.Assert(inputs != null);
                    for (var i = 0; i < request.Arguments.Count; i++)
                    {
                        var arg = request.Arguments[i];
                        if (arg == null)
                        {
                            continue;
                        }
                        var builtinType = inputs[i].Item1.BuiltInType;
                        if (!string.IsNullOrEmpty(arg.DataType))
                        {
                            builtinType = await TypeInfo.GetBuiltInTypeAsync(
                                arg.DataType.ToNodeId(context.Session.MessageContext),
                                context.Session.TypeTree, context.Ct).ConfigureAwait(false);
                        }
                        requests[0].InputArguments[i] = context.Session.Codec.Decode(
                            arg.Value ?? VariantValue.Null, builtinType);
                    }
                }

                // Call method
                var response = await context.Session.Services.CallAsync(
                    request.Header.ToRequestHeader(_timeProvider), requests,
                    context.Ct).ConfigureAwait(false);

                var results = response.Validate(response.Results, r => r.StatusCode,
                    response.DiagnosticInfos, requests);
                if (results.ErrorInfo != null)
                {
                    return new MethodCallResponseModel
                    {
                        Results = Array.Empty<MethodCallArgumentModel>(),
                        ErrorInfo = results.ErrorInfo
                    };
                }
                // Create output argument list
                var arguments = new List<MethodCallArgumentModel>();

                var args = results[0].Result.OutputArguments?.Count ?? 0;
                for (var i = 0; i < args; i++)
                {
                    var arg = results[0].Result.OutputArguments[i];
                    if (arg == Variant.Null &&
                        (outputs?.Count ?? 0) > i && outputs![i].Item2 != null)
                    {
                        // return default value
                        arg = new Variant(outputs[i].Item2);
                    }
                    var value = context.Session.Codec.Encode(arg, out var type);
                    if (type == BuiltInType.Null && (outputs?.Count ?? 0) > i)
                    {
                        // return default type from type info
                        type = outputs![i].Item1.BuiltInType;
                    }
                    var dataType = type == BuiltInType.Null ?
                        null : type.ToString();
                    arguments.Add(new MethodCallArgumentModel
                    {
                        Value = value,
                        DataType = dataType
                    });
                }
                return new MethodCallResponseModel
                {
                    Results = arguments,
                    ErrorInfo = results[0].ErrorInfo
                };
            }, request.Header, ct).ConfigureAwait(false);
        }