Skip to content

Tutorial 04 08 Add Helper Service Class

Matt Linder edited this page Jun 1, 2023 · 37 revisions

Harmony Core Logo

Add a Helper Service Class

For each routine that you expose through Traditional Bridge on the traditional Synergy side, there needs to be a client-callable wrapper method on the .NET side of the Harmony Core solution. Ultimately, the Synergy routines must be exposed as web service endpoints. Or put another way, they need to be exposed as methods in a controller class in your Services.Controllers project, so that's where the wrappers need to be added.

Add the Service Class

The service class must inherit from the class Harmony.Core.Context.DynamicCallProvider. This class contains the types and code necessary to be a client to a Traditional Bridge host application, by marshaling method calls into JSON-RPC requests, sending the request, receiving and decoding the response, and then handing appropriate response data back to the calling routine. This class was generated for us when we generated code after adding the BridgeMethods interface, so it is named BridgeMethodsService.

  1. In Solution Explorer, right-click Services.Controllers and select Add > Existing Item from the context menu.

  2. In the Add Existing Item dialog, navigate to the Services.Controllers folder, select BridgeMethodsService.dbl, and click Add.

  3. Open the BridgeMethodsService.dbl in Visual Studio and notice that the code includes a constructor method that takes advantage of the Dependency Injection environment that is provided by Harmony Core. In this case, the method requests an instance of IDynamicCallConnection, which represents a connection from Harmony Core to a Traditional Bridge host application, which is passed directly into the base class via a constructor initializer.

    public method BridgeMethodsService
        connection, @IDynamicCallConnection
        endparams
        parent(connection)
    proc
        if(!IsInitialized)
            throw new Exception("cctor missing")
    endmethod
    

Helper Methods

Note that the code also includes a helper method for each of the routines exposed by your Traditional Bridge environment. The basic idea is that when code in a controller class calls a Traditional Bridge routine, it will obtain a copy of this class via dependency injection and call the appropriate wrapper method. Note that all helper methods must be public async methods.

Parameters

Helper methods must have parameters that match the parameters of the underlying traditional Synergy routine. Remember that we're in .NET here, and the code is calling a traditional Synergy routine, so the parameter data types are often not a direct match. For example, you would generally use a string parameter in .NET when interacting with an alpha field in traditional Synergy.

Return Value

Because an async pattern is being used in this class, all helper methods must return a Task object. If the underlying traditional Synergy routine is a subroutine, the return value of the helper method should be defined as @Task.

If the underlying traditional Synergy routine is a function, the return value of the helper method should be defined as @Task<T>, with the generic type T being based on the return value type of the function. Again, appropriate mappings between traditional Synergy and .NET need to be used.

CallMethod and Tuple

The actual call to the underlying traditional Synergy routine is done by calling an inherited method named CallMethod, and the return value of this method is a tuple. A tuple is a data structure that has a specific number and sequence of elements, and those elements can be of different types. An example of a tuple is a data structure with three elements. Item 1 might be an integer, item 2 a decimal, and item 3 a string. When dealing with a tuple, the items are always named Item1, Item2, and so on.

It's not important that you know how this particular tuple is structured because Harmony Core provides a static helper class named ArgumentHelper to assist you in decoding returned parameters and function return values. The helper class has a generic method named Argument, and it accepts two parameters:

  • The first parameter is the return value or argument number, where 0 is the function return value, 1 is argument 1, 2 is argument 2, and so on.

  • The second parameter is the Tuple returned by CallMethod.

Here is an example of using the helper to extract the value of an alpha function return value:

data retval = ArgumentHelper.Argument<string>(0,returnTuple)

And here is an example of using the helper to extract the numeric value of out or inout parameter 3:

data param3value = ArgumentHelper.Argument<decimal>(3,returnTuple)

Sample Wrapper Code

Notice that each client-callable wrapper is a public async method in the BridgeMethodsService class. Client-callable wrappers

  • return an @Task or @Task value.
  • call the traditional Synergy routine.
  • translate argument types from the types used in the OData calls to the Synergy types used in the traditional Synergy routine. If a traditional Synergy routine has more than one argument type, the client-callable wrapper must translate each one.

These translations are described in the sections that follow:

Wrapper Code for Primitive Data Types

Parameters being represented in .NET as integer, float, double, decimal and string are translated into underlying traditional Synergy data types on the server side. This means that client-callable wrapper code for these types is very simple. For example:

public async method PassParameters, @Task<string>
    intParam,    int
    stringParam, string
    decParam,    decimal
proc
    ;; CallMethod takes the method name to call along with the parameters and a dummy value
    ;; used to determine the expected return type if there is a return value.
    data resultTuple = await CallMethod("PassParameters", intParam, stringParam, decParam, String.Empty)

    ;; Return the string from the return value
    mreturn ArgumentHelper.Argument<string>(0,resultTuple)

endmethod

Wrapper Code for Structures

To support structure arguments in Traditional Bridge, you must have matching data object classes (classes that extend DataObjectBase) on the server (traditional Synergy code) and the client (Synergy .NET code). Start by generating the client-side data object classes:

  • In the Harmony Core GUI tool, do the following:

    • Add the structures in the Structures screen of the Harmony Core GUI tool (see Selecting the Structure(s) to Include). For the client-side, CodeGen uses the same data object templates used elsewhere in .NET code: ODataMetadata.tpl and ODataModel.tpl.

    • Make sure the Enable Traditional Bridge option is set for the project. This option is on the Traditional Bridge screen of the Harmony Core GUI tool.

    • If your traditional Synergy routines have optional parameters (see Wrapper Code for Optional Parameters below), set the Enable optional parameters option on the Traditional Bridge screen in the Harmony Core GUI tool.

  1. Save these changes by selecting File > Save from the menu.

  2. Generate code for the solution by selecting Codegen > Regen from the menu.

Once client-side data object classes are generated, use these classes in the client-side wrapper (which is BridgeMethodsService for this tutorial), in the same way you would pass a primitive argument type. For example, the following passes the Customer data object:

public async method ProcessCustomerStructure, @Task<Customer>
    customer, @Customer
proc
    ;; CallMethod takes the method name to call along with the parameters and a dummy value
    ;; used to determine the expected return type if there is a return value
    data resultTuple = await CallMethod("ProcessCustomerStructure", customer)

    ;; Return the customer object from parameter 1
    mreturn ArgumentHelper<Customer>(1,resultTuple)

endmethod

Wrapper Code for Collections

Collection support on the server is currently limited to ArrayList, memory handles, dynamic arrays, and pseudo arrays. This support allows for element types of a collection to be primitives, Synergy structures, or data objects. For example:

public async method GetAllCustomers, @Task<List<Customer>>

proc
    ;;CallMethod takes the method name to call along with the parameters and a dummy value
    ;;used to determine the expected return type if there is a return value
    data resultTuple = await CallMethod("GetAllCustomers", new List<Customer>())

    ;; Return the collection of customer objects from parameter 1
    mreturn ArgumentHelper.Argument<IEnumerable<Customer>>(1,resultTuple)

endmethod

Wrapper Code for Optional Parameters

For optional parameters, Traditional Bridge supports the primitive arguments a, d, i, and n. This approximates the type support for xfNetLink COM. Traditional Synergy code written for xfNetLink COM access frequently uses optional parameters, so if your code was originally written for xfNetLink COM, there may need to be code for optional parameters. The following is an example of this:

public partial class TraditionalBridgeService extends DynamicCallProvider

    public async method Arbitrario_Optional, @Task<ArbitrarioOptionalReturnType>
        parm, @ArbitrarioOptionalParameter
proc
        ;;calls to ArgumentHelper.MaybeOptional translate nulls into not-passed data types under the hood.
        data resultTpl = await CallMethod("arbitrario_optional", parm.p1, ArgumentHelper.MaybeOptional(parm.p2), ArgumentHelper.MaybeOptional(parm.p3), ArgumentHelper.MaybeOptional(parm.p4))
        data resultArray = resultTpl.Item2.ToList()
        data returnValue = new ArbitrarioOptionalReturnType()
        returnValue.p3 = ^as(resultArray[2], string)

        ;;If a value type is optional, like the int below, you will need to define it as nullable to allow an un-passed value.

        returnValue.p4 = ^as(resultArray[3], Nullable<int>)

        mreturn returnValue

    endmethod

    public class ArbitrarioOptionalParameter
        public readwrite property p1, int
        public readwrite property p2, string
        public readwrite property p3, string
        public readwrite property p4, int?
    endclass

    public class ArbitrarioOptionalReturnType
        public readwrite property p3, string
        public readwrite property p4, int?
    endclass

endclass

Writing the Wrapper Code

Wrapper Code for GetEnvironment

  1. Examine the wrapper code for the GetEnvironment routine:

     ;;; <summary>
     ;;; Get environment string
     ;;; </summary>
     public async method GetEnvironment, @Task<BridgeMethods.GetEnvironment_Response>
     proc
         ;;Prepare the response object
         data response = new BridgeMethods.GetEnvironment_Response()
         ;;Make the JSON-RPC call the traditional Synergy routine
         data resultTuple = await CallMethod("GetEnvironment"
         &   )
             ;;Set the return value in the return data
         ArgumentHelper.Argument(0, resultTuple, response.ReturnValue)
         ;;Return the response
         mreturn response
     endmethod
    

Note that the second statement calls the underlying traditional Synergy routine, assigning the information returned by the routine into a Tuple named resultTuple. And the second statement extracts the first item from the tuple (the return value) and returns it to the calling routine.

Wrapper Code for GetLogicalName

  1. Next, examine the wrapper code fot the GetLogicalName routine:

     ;;; <summary>
     ;;; Get a logical names value
     ;;; </summary>
     public async method GetLogicalName, @Task<BridgeMethods.GetLogicalName_Response>
         required in args, @BridgeMethods.GetLogicalName_Request
     proc
         ;;Prepare the response object
         data response = new BridgeMethods.GetLogicalName_Response()
         ;;Make the JSON-RPC call the traditional Synergy routine
         data resultTuple = await CallMethod("GetLogicalName"
         &   ,args.aLogicalName
         &   )
             ;;Set the return value in the return data
         ArgumentHelper.Argument(0, resultTuple, response.ReturnValue)
         ;;Return the response
         mreturn response
     endmethod
    

The code in this example is very similar to the preceding example, except for the addition of a single inbound parameter.

Wrapper Code for AddTwo numbers

  1. Finally, take a look at the wrapper code for the AddTwo numbers routine:

     ;;; <summary>
     ;;; Add two numbers
     ;;; </summary>
     public async method AddTwoNumbers, @Task<BridgeMethods.AddTwoNumbers_Response>
         required in args, @BridgeMethods.AddTwoNumbers_Request
     proc
         ;;Prepare the response object
         data response = new BridgeMethods.AddTwoNumbers_Response()
    
         ;;Make the JSON-RPC call the traditional Synergy routine
         data resultTuple = await CallMethod("AddTwoNumbers"
         &   ,args.number1
         &   ,args.number2
         &   ,response.result
         &   )
    
         ArgumentHelper.Argument(3, resultTuple, response.result)
    
         ;;Return the response
         mreturn response
    
     endmethod
    
    
    

The code in this example is similar to the previous examples, except that there are now two inbound parameters, and the underlying traditional Synergy routine exposes a third parameter, which is an out parameter. In this scenario we must pass a variable to CallMethod, a variable that indicates the type of the additional parameter that is expected to be returned. Note that this variable is not updated with the actual returned value by CallMethod. As in previous examples out, parameters are encoded into the returned tuple.

That's all you can do for now because you don't yet have any code that attempts to call the helper methods. Later you will add some code to the Startup class to register your TraditionalBridgeService as a Service within the Dependency Injection container.


Next topic: Add Controller Class


Clone this wiki locally