Skip to content

Traditional Bridge

Jeff Greene edited this page Jul 16, 2021 · 37 revisions

Harmony Core Logo

Traditional Bridge

Traditional Bridge is a mechanism that allows traditional Synergy code (external subroutines and functions) to be incorporated into and exposed as part of a Harmony Core Web Service. The traditional Synergy code can execute either on the same system as the Harmony Core service, or on another networked Windows, UNIX, Linux or OpenVMS system.

The protocol used to interact between Jarmony Core and Traditional Synergy code is known as the Dynamic Call Protocol.

The Big Picture

At a high level, this is how a Harmony Core Traditional Bridge environment works:

  • A traditional Synergy subroutine or function that implements some useful piece of functionality either already exists, or is created. For example, here is a simple routine that adds two numbers together. As you can see the routine has two input parameters to allow two numeric values to be provided, and a third output parameter to allow the sum of the two numbers to be returned.
subroutine AddTwoNumbers
    required in  number1, n
    required in  number2, n
    required out result,  n
proc
    result = number1 + number2
    xreturn
endsubroutine
  • To expose the routine you start by adding a custom endpoint to some controller class in your RESTful web service. For the routine we just looked at you might add an endpoint that is accessed via a URL something like this:
https://my.server.com/functions/AddTwoNumbers/1/2
  • When a client accesses the endpoint, code runs in the web service that utilizes functionality provided by Harmony Core to:

    • Generate a request message that identifies the function to be called and includes the data provided (1 and 2 in this case).
    • Instantiate a copy of a traditional Synergy program that is able to call the underlying function.
    • Send the message to the program and wait for a response message.
    • Extract data (if any) from the response message, or deal with exceptions.
  • Code in the web service endpoint then returns an appropriate response to the client application.

Local and Remote Traditional Bridge

Traditional Bridge can be implemented in two ways, depending on where you wish to execute the traditional Synergy code.

Local Traditional Bridge

When implemented locally the traditional Synergy code is built to run on Windows, on the same Windows server that hosts the Harmony Core web services. The Harmony Core service launches the Traditional Bridge host program in a local process.

Local Traditional Bridge

Remote Traditional Bridge

A remote implementation occurs when the traditional Synergy code is executed on a remote system. That system can be any system with traditional Synergy support, including Windows, Unix, Linux and OpenVMS. The Harmony Core service makes an SSH connection to the remote system and logs in the same way a user in a terminal emulator would, then sends a command to launch the Traditional Bridge host program in a process on that remote system.

Remote Traditional Bridge

Communicating with the Traditional Synergy Process

Regardless of whether a Traditional Bridge process is started locally or remotely, communication with the process occurs the same way. Messages (instructions to execute a subroutine or function) are sent to the traditional Synergy process via its standard input channel (stdin) and the process responds by sending appropriately formatted messages via its standard output channel (stdout).

This is exactly the same mechanism as you use when you use a terminal emulator to log in to a remote system. You type commands to the process (stdin), and the response is displayed on the screen (stdout).

The Messaging Protocol

The format of the messages sent to and received from the traditional Synergy process defined by a protocol called JSON-RPC 2.0. You don't need to know this protocol (we provide code to deal with it) but if you are interested you can find more information on the Dynamic Call Protocol page.

Here's an example JSON-RPC message that invokes the AddTwoNumbers function described above:

{
    "jsonrpc": "2.0",
    "method": "AddTwoNumbers",
    "params": [
    {
        "PassedValue": "1",
        "DataType": 4,
        "ElementSize": 56,
        "DecimalPrecision": 28
    },
    {
        "PassedValue": "2",
        "DataType": 4,
        "ElementSize": 56,
        "DecimalPrecision": 28
    },
    {
        "PassedValue": "0",
        "DataType": 4,
        "ElementSize": 56,
        "DecimalPrecision": 28
    }],
    "id": 2
}

As you can see, the values to be added (1 and 2 in this case) are passed via the first two parameters. The response message from the Traditional Bridge call looks like this:

{
    "jsonrpc": "2.0",
    "id": 2,
    "result": [
    {
        "Position": 3,
        "Value":
        {
            "DataType": 4,
            "ElementSize": 56,
            "DecimalPrecision": 28,
            "PassedValue": 3.0
        }
    }
  ]
}

As already mentioned, when you're implementing a Traditional Bridge environment you won't be dealing with JSON data at a low level like this. Harmony Core Traditional Bridge provides a library of routines that take care of the actual sending and receiving of messages, and serialization and deserialization of data and parameters to and from JSON.

Traditional Bridge Example

Let's walk through a simple example of exposing a traditional Synergy subroutine via Traditional Bridge.

Routine to be Exposed (Traditional Synergy)

The first thing to do is to identify or create the traditional Synergy routine that is to be exposed. Traditional Bridge is essentially implemented as a standalone DBR that is executed by Harmony Core, which then sends instructions to the running program via the terminal channel (i.e. via stdin and stdout) to ask the program to execute the subroutine. That means that the subroutine needs to be either linked in to the DBR program, or in an ELB that the DBR program is linked against.

For this example we will use the simple AddTwoNumbers subroutine that was shown earlier:

;;*****************************************************************************
;;
;; Title:       AddTwoNumbers.dbl
;;
;; Description: An example routine being exposed by Traditional Bridge.
;;              Calculates and returns the sum of two numbers.
;;
;;*****************************************************************************

subroutine AddTwoNumbers
    required in  number1, n
    required in  number2, n
    required out result,  n
proc
    result = number1 + number2
    xreturn
endsubroutine

Routine Dispatcher Class (Traditional Synergy)

The next step in exposing a routine via Traditional Bridge is to create a routine dispatcher class for the routine. This may sound complicated, but actually it is not. The dispatcher class simply needs to inherit from a parent class called DynamicCallProvider that is provided by the Traditional Bridge library code, and contain a single method called 'DispatchInternal'. Here is an example:

;;*****************************************************************************
;;
;; Title:       AddTwoNumbersDispatcher.dbl
;;
;; Description: Dispatcher class for calls to AddTwoNumbers
;;
;;*****************************************************************************

import Harmony.TraditionalBridge
import Json
import System
import System.Collections

.ifdef DBLV11
import System.Text.Json
.define JSON_ELEMENT @JsonElement
.else
.define JSON_ELEMENT @JsonValue
.endc

namespace TraditionalBridge.Dispatchers

    ;;; <summary>
    ;;; Dispatcher for AddTwoNumbers
    ;;; </summary>
    public class AddTwoNumbersDispatcher extends RoutineStub

        ;;; <summary>
        ;;; Dispatch to AddTwoNumbers
        ;;; </summary>
        ;;; <param name="name">Name of routine to call.</param>
        ;;; <param name="callFrame">Current JSON-RPC request.</param>
        ;;; <param name="serializer">Outbound data serializer.</param>
        ;;; <param name="dispatcher">Inbound routine dispatcher.</param>
        protected override method DispatchInternal, void
            required in name,       string
            required in callFrame,  JSON_ELEMENT
            required in serializer, @DispatchSerializer
            required in dispatcher, @RoutineDispatcher

            ;;------------------------------------------------------------
            ;; Declare local variables for parameters and/or return value


        proc
            ;;------------------------------------------------------------
            ;; Process inbound parameters


            ;;------------------------------------------------------------
            ;; Call the underlying routine


            ;;------------------------------------------------------------
            ;; Process any outbound return value and/or parameters


        endmethod

    endclass

endnamespace

As you can see, several parameters are passed into the DispatchInternal method in your routine dispatcher class. These parameters give your code access to the inbound parameters from Harmony Core, via the callFrame property, access to the outbound parameters, via the serializer parameter, and access to various helper methods to assist in dealing with parameters of various data types, via the dispatcher parameter. The method also receives the name of the routine to be called, making it possible to write a single routine dispatcher class to deal with calls to several different traditional Synergy routines, if necessary.

The DispatchInternal method is usually responsible for doing four things:

  1. Declaring appropriate local variables that can be used to: a. Pass any inbound parameters on to the routine. b. Capture the value of any outbound parameters and/or return value.

  2. Extract the values of any inbound parameters from the callFrame object and store the values in the corresponding local variables.

  3. Calling the underlying traditional Synergy routine, passing and receiving parameters, and any return value via the local variables.

  4. Providing any outbound parameters and/or return value back to Harmony Core via the serializer object.

Here is an example of a complete routine dispatcher needed to call the AddTwoNumbers subroutine:

;;*****************************************************************************
;;
;; Title:       AddTwoNumbersDispatcher.dbl
;;
;; Description: Dispatcher class for calls to AddTwoNumbers
;;
;;*****************************************************************************

import Harmony.TraditionalBridge
import Json
import System
import System.Collections

.ifdef DBLV11
import System.Text.Json
.define JSON_ELEMENT @JsonElement
.else
.define JSON_ELEMENT @JsonValue
.endc

namespace TraditionalBridge.Dispatchers

    ;;; <summary>
    ;;; Dispatcher for AddTwoNumbers
    ;;; </summary>
    public class AddTwoNumbersDispatcher extends RoutineStub

        ;;; <summary>
        ;;; Dispatch to AddTwoNumbers
        ;;; </summary>
        ;;; <param name="name">Name of routine to call.</param>
        ;;; <param name="callFrame">Current JSON-RPC request.</param>
        ;;; <param name="serializer">Outbound data serializer.</param>
        ;;; <param name="dispatcher">Inbound routine dispatcher.</param>
        protected override method DispatchInternal, void
            required in name,       string
            required in callFrame,  JSON_ELEMENT
            required in serializer, @DispatchSerializer
            required in dispatcher, @RoutineDispatcher

            ;; Declare data for any parameters and/or return value
            record
                parameters,         JSON_ELEMENT
                par1,               decimal
                par2,               decimal
                par3,               decimal
            endrecord
        proc
            ;;------------------------------------------------------------
            ;; Process inbound parameters

            parameters= callFrame.GetProperty("params")

            ;;Parameters 1 and 2 are passed in to the underlying routine
            par1 = dispatcher.GetImplied(parameters[1])
            par2 = dispatcher.GetImplied(parameters[2])

            ;;------------------------------------------------------------
            ;; Call the underlying routine

            xcall AddTwoNumbers(par1,par2,par3)

            ;;------------------------------------------------------------
            ;; Process any outbound return value and/or parameters

            ;;Parameter 3 is the returned value
            serializer.ArgumentData(3,par3,FieldDataType.ImpliedDecimal,28,10,false)

        endmethod

    endclass

endnamespace

The AddTwoNumbers routine has two IN parameters, one OUT parameter, and no return value. So the code declares three variables named par1, par2, and par3, using appropriate data types for the routine being called. An additional local variable named parameters is also defined and will be used to provide access to the parameters passed via the callFrame. You will need to use a similar approach whenever you have inbound parameters.

As you can see, the code extracts the value of inbound parameters, before calling the underlying routine and passing the parameters.

Once the routine returns, the code uses the serializer object to pass back any outbound parameters and/or return value, which isn't used in this case, but would be referred to as parameter 0.

Parameter Helper Methods (Traditional Synergy)

An instance of the RoutineDispatcher class (discussed below) which is passed to your routine dispatcher classes via the RoutineDispatcher parameter (see above) provides the following helper methods to assist you in extracting inbound parameter information from the passed callFrame.

  • GetText
  • GetInt
  • GetDecimal
  • GetImplied
  • DeserializeObject
  • DeserializeObjectCollection

Master Dispatcher Class (Traditional Synergy)

Once you have a routine dispatcher class for each of your exposed routines, you must create a master dispatcher class that registers and hosts instances of each of your routine dispatchers. A master dispatcher class is very simple, here is an example of a master dispatcher that registers the routine dispatcher for the AddTwoNumbers routine:

;;*****************************************************************************
;;
;; Title:       MethodDispatcher.dbl
;;
;; Description: Declares dispacher classes for exposed routines
;;
;;*****************************************************************************

import Harmony.TraditionalBridge

namespace TraditionalBridge.Dispatchers

    public partial class MethodDispatcher extends RoutineDispatcher

        public method MethodDispatcher
        proc
            ;; Register routine dispatcher classes 
            mDispatchStubs.Add("AddTwoNumbers",  new AddTwoNumbersDispatcher())

        endmethod

    endclass

endnamespace

Each call to the mDispatchStubs.Add method provides the name of the traditional Synergy routine being exposed, and an instance of the routine dispatcher class to be used to call the routine; one such line of code would be added for each traditional Synergy routine being exposed.

Hosting Program (Traditional Synergy)

The only remaining task on the traditional Synergy side is to write the code for a program that will host your Traditional Bridge service. This program will usually be a simple DBR application that opens the terminal channel, listens for (JSON-RPC) instructions on that channel, dispatches requests for the execution of exposed routines, via the various dispatcher classes, and sends the response (JSON-RPC) back down the terminal channel.

Most of the code required to achieve all of these things is either provided by the Traditional Bridge library code, or by the dispatcher classes and underlying Synergy routines that have already been discussed. This means that the actual hosting program is typically very simple, as you can see here:

;;*****************************************************************************
;;
;; Title:       TraditionalBridgeHost.dbl
;;
;; Description: A program to host a Harmony Core Traditional Bridge environment
;;              that exposes traditional Synergy business logic to a Harmony
;;              Core web service.
;;
;;*****************************************************************************

import Harmony.TraditionalBridge
import Json
import TraditionalBridge
import TraditionalBridge.Dispatchers

main TraditionalBridgeHost
    record
        ttChan,         i4                  ;;Terminal channel number
        length,         int                 ;;Length of a string
        tmpa,           a10                 ;;Temporary alpha variable
        logLevel,       int                 ;;Logging level.
        logFileName,    string              ;;Log file name
        dispatcher,     @MethodDispatcher   ;;Message dispatcher instance
    endrecord

proc
    try
    begin
        ;;Configure the environment and open the terminal chanel.
        ;; - STDIN  is the mechanism by which Harmony Core sends us requests
        ;; - STDOUT is the mechanism by which we will issue responses
        xcall flags(1907050020)
        open(ttChan=0,o,"TT:")

        ;;Does the environment define the logging level?
        xcall getlog("HARMONY_LOG_LEVEL", tmpa, length)
        if (length) then
        begin
            logLevel = %integer(tmpa)
        end
        else
        begin
            ;Levels are 1 to 6, 2 is normal logging, 6 is highest logging
            logLevel = 2
        end

        ;;Define the log file name
        logFileName = "BRIDGE_" + %string(%jbno) + ".LOG"

        ;;Initiate logging
        Logger.Instance = new Logger(logFileName, logLevel, false)

        ;;Create a new instance of the main "dispatcher" class
        dispatcher = new MethodDispatcher()
     
        ;;Issue the "READY" message.
        ;;Harmony Core looks for this and considers the connection active when it sees it
        puts(ttChan, "READY" + %char(13)+ %char(10))

        ;;Start dispatching requests
        dispatcher.Dispatch(ttChan)


    end
    catch (e, @Exception)
    begin
        ;;If anything failed, log an error if we can
        if (Logger.Instance != ^null)
        begin
            Logger.Instance.Log("DISPATCHER EXITED with exception" + e.ToString())
        end

    end
    endtry

    ;;Close the log
    if (Logger.Instance != ^null)
    begin
        Logger.Instance.CloseLog()
    end

    stop

endmain

Other than swapping out the type of your master dispatcher class (@MethodDispatcher in the above code) in the data division, and changing the type in the statement that creates an instance of the master dispatcher (new MethodDispatcher() in the code above), this hosting code should bu all you need.

All the code actually does is establish an appropriate logging environment, then instantiate and call into the master dispatcher class (dispatcher.Dispatch(ttChan) in the code above).

By the way, notice how the code displays the word READY to the terminal channel just before calling into the master dispatcher class? This is really important, because Harmony Core, which is listening on the other end of the terminal channel, looks for READY as a signal that everything is up and running and ready to accept requests. So don't change that part!

Service Class (.NET)

On the .NET side of a Traditional Bridge implementation, the first thing you need to implement is a Service class in your Services.Controllers project. This new class will be registered as a service within the ASP.NET Core dependency injection container, thus making it easily accessible throughout the Harmony Core service. The class needs to expose public methods associated with each of the traditional Synergy routines being exposed by the Traditional Bridge implementation. Here is what the service class would look like for our Traditional Bridge service that exposes the AddTwoNumbers routine:

;;*****************************************************************************
;;
;; Title:       TraditionalBridgeService.dbl
;;
;; Description: Exposes example traditional Synergy routines via Traditional
;;              Bridge JSON-RPC calls.
;;
;;*****************************************************************************

import Harmony.Core
import Harmony.Core.Context
import Harmony.Core.Interface
import Harmony.Core.EF.Extensions
import Harmony.OData
import Harmony.OData.Adapter
import System.Threading.Tasks
import System
import System.Collections.Generic
import System.Text
import Services
import Services.Models
import Microsoft.AspNetCore.Mvc
import Microsoft.AspNet.OData
import Microsoft.AspNetCore.Authorization
import Newtonsoft.Json.Linq
import System.Linq

import Services.Controllers
import Services.Models

namespace Services.Controllers

    public partial class TraditionalBridgeService extends DynamicCallProvider

        ;;; <summary>
        ;;; Constructor
        ;;; </summary>
        public method TraditionalBridgeService
            connection, @IDynamicCallConnection
            parent(connection)
        proc
         
        endmethod

        public async method AddTwoNumbers, @Task<decimal>
            required in aNumber1, decimal
            required in aNumber2, decimal
        proc
            ;; Prepare a variable indicating the type of the returned third parameter
            data sum, decimal

            ;; Call the method passing parameters and return value type
            data resultTuple = await CallMethod("AddTwoNumbers",aNumber1,aNumber2,sum)

            ;; Extract the result from parameter 3
            sum = ArgumentHelper.Argument<decimal>(3,resultTuple)

            ;; And return it
            mreturn sum 

        endmethod

    endclass

endnamespace

As you can see, the service class inherits from a class called DynamicCallProvider that contains most of the complex code needed to construct and issue the JSON-RPC message that will be sent to your Traditional Bridge host program to cause the execution of a routine.

Notice how the constructor method receives an instance of IDynamicCallConnection and passes it on into the parent code. This object represents an established connection to your traditional Synergy environment, where your Traditional Bridge host program will already be running and listening for requests.

Beyond that, the task is to add a "wrapper" method for each of your exposed routines. Here is the signature of the wrapper method for the AddTwoNumbers routine.

public async method AddTwoNumbers, @Task<decimal>
            required in aNumber1, decimal
            required in aNumber2, decimal

Notice that parameters are defined for the inbound parameters, but the outbound parameter is passed via the return value. This pattern is necessary because the wrapper methods must all be ASYNC methods, and async methods can only have one outbound value or none at all.

If the routine that you are calling returns more than one value then you will need to somehow return all of those values via a single object instance, for example, you might choose to return an array or some kind of collection, or a tuple, or perhaps even a custom object with properties to represent the outbound values. The best approach really depends on the nature of the out parameters and/or return value from the underlying routine.

If the routine being called does not have any outbound data, then simply declare the return type of the wrapper methods as @Task.

By default, Traditional Bridge uses pooled processes/connections. If you're migrating from xfServerPlus COM+ pooling, you can implement hooks for many of the same lifecycle events. This following methods can be overriden in your service wrapper class.

public virtual method Destroy, void
	endparams
proc
	;This is called when a process/connection is about to be destroyed
endmethod

public virtual method InitServices, void
	sp, @IServiceProvider
proc
	;This is called when an process/connection is handed out. The IServiceProvider will be specific to the current request
endmethod

public virtual method Recycle, @Task
proc
	;implement any custom logic here required to support recycling this object
	mreturn Task.FromResult(true)
endmethod

public virtual property IsHealthy, boolean
	method get
	proc
		;return false if this process/connection should be discarded rather than recycled
		mreturn true
	endmethod
endproperty

Registering and Starting the Service (.NET)

Once you have created a custom service class, you must register that class into the ASP.NET Core dependency injection container that is used by the Harmony Core service. This should be done in custom code in the Startup class in your Services project.

If you already have a custom startup file then you can add the code into that existing file. If not then you must add a custom Startup file and use the partial class mechanism to provide custom code. We recommend naming your custom startup file Startup.Custom.dbl, and when first created it should look like this:

import Harmony.AspNetCore
import Harmony.AspNetCore.Context
import Harmony.Core
import Harmony.Core.Context
import Harmony.Core.FileIO
import Harmony.Core.Interface
import Harmony.Core.Utility
import Harmony.OData
import Harmony.OData.Adapter
import Microsoft.AspNetCore.Authorization
import Microsoft.AspNetCore.Authentication.JwtBearer
import Microsoft.AspNetCore.Builder
import Microsoft.AspNetCore.Hosting
import Microsoft.AspNetCore.Http
import Microsoft.AspNetCore.Mvc
import Microsoft.AspNetCore.Mvc.Abstractions
import Microsoft.AspNetCore.Mvc.ApiExplorer
import Microsoft.AspNetCore.StaticFiles
import Microsoft.AspNet.OData
import Microsoft.AspNet.OData.Extensions
import Microsoft.AspNet.OData.Builder
import Microsoft.AspNet.OData.Formatter
import Microsoft.AspNet.OData.Routing
import Microsoft.AspNet.OData.Routing.Conventions
import Microsoft.EntityFrameworkCore
import Microsoft.Extensions.Configuration
import Microsoft.Extensions.DependencyInjection
import Microsoft.Extensions.DependencyInjection.Extensions
import Microsoft.Extensions.Logging
import Microsoft.Extensions.Options
import Microsoft.Extensions.Primitives
import Microsoft.IdentityModel.Tokens
import Microsoft.Net.Http.Headers
import Microsoft.OData
import Microsoft.OData.Edm
import Microsoft.OData.UriParser
import System.Collections.Generic
import System.IO
import System.Linq
import System.Text
import System.Threading.Tasks
import Services.Controllers
import Services.Models
import Swashbuckle.AspNetCore.Swagger
import Microsoft.OpenApi.Models

namespace Services

    public partial class Startup
	
        partial method ConfigureServicesCustom, void
            services, @Microsoft.Extensions.DependencyInjection.IServiceCollection
        proc

        endmethod

    endclass

endnamespace

All of those import statements were copied from the main Startup class; they may be overkill here, but they should provide most custom code with all of the namespaces that are likely to be required.

Notice that the class definition in this file is public partial class Startup; we're not defining a new class here, we are providing additional code into the existing Startup class that is defined in Startup.dbl.

Also, notice the name of the method that is included in the sample code above, which is ConfigureServicesCustom. This is what is known as a partial method that is already defined in the main Startup class, and there is code that will cause the method to be called if present. So simply by adding the method into the partial class, it will be called during startup.

All that remains is to add code to create an instance

namespace Services

    public partial class Startup
	
        partial method ConfigureServicesCustom, void
            services, @Microsoft.Extensions.DependencyInjection.IServiceCollection
        proc
            ;;----------------------------------------------
            ;; Configure the Traditional Bridge environment.

            data traditionalBridgeDirectory, string
            data logLevel, string

            if (_env.IsDevelopment()) then
            begin
                traditionalBridgeDirectory=Environment.GetEnvironmentVariable("EXEDIR")
                if (String.IsNullOrWhiteSpace(traditionalBridgeDirectory))
                begin
                    throw new ApplicationException("Logical name EXEDIR is not set. It must point to the TraditionalBridge host application.")
                end
                logLevel = "6"
            end
            else
            begin
                traditionalBridgeDirectory = Path.GetFullPath(Path.GetDirectoryName(^typeof(Startup).Assembly.Location))
                logLevel = "2"
            end

            data launchCommand, string, Path.Combine(traditionalBridgeDirectory,"launch.bat")
            data environmentVariables, @Dictionary<string,string>,new Dictionary<string,string>(){{"HARMONY_LOG_LEVEL",logLevel}}

            ;;----------------------------------------------------------
            ;; Make 'TraditionalBridgeService' available as a DI service

            data contextPool, @ExternalContextPool<TraditionalBridgeService>, new ExternalContextPool<TraditionalBridgeService>
            &    (
            &    launchCommand,
            &    "optional_command_parameters",
            &    traditionalBridgeDirectory,
            &    environmentVariables,
            &    4
            &    )

            services.AddSingleton<IContextFactory<TraditionalBridgeService>>(contextPool)
            services.AddContextPool<TraditionalBridgeService>()

        endmethod

    endclass

endnamespace

If you walk through this code you will see that it basically determines the location of the Traditional Bridge files (primarily the hosting program) and prepares a command that can be used to launch the hosting program. In this case the example is configuring the Traditional Bridge environment to be on the local Windows server, so a batch file named launch.bat will be used to prepare the Synergy environment and then launch the DBR host program.

The code then creates a new instance of ExternalContextPool, telling it that the type of service being hosted is TraditionalBridgeService (the Service class created earlier), and passing various information such as the launch command to be used, the directory where the command should be executed, and a collection of logical names to be set in the environment.

Remember, this environment will launch a local windows process, but in other configurations, an SSH connection to a remote machine might be made, and the launch command would be whatever command is necessary to launch the Traditional Bridge host program on the remote system. Starting processes on Linux, UNIX and OpenVMS systems is much simpler, because the environment can be setup via a login script (perhaps .profile, .bashprofile or LOGIN.COM). But on Windows, where there typically is no default Synergy environment on login, it is generally easier to launch a batch file to set the environment and start the host program.

Once instantiated, the ExternalContextPool and TraditionalBridgeService are added to the services collection and are then available for use throughout the Harmony Core service

Starting the Traditional Bridge Service (Remote Windows Process)

Coming soon!

Starting the Traditional Bridge Service (Remote Linux Process)

Coming soon!

Starting the Traditional Bridge Service (Remote OpenVMS Process)

Coming soon!

Controller Class (.NET)

The final step in the process is to expose actual web service endpoints to consumers of the application. This is typically done by adding a ASP.NET Core Web API controller to the service. Here is an example:

;;*****************************************************************************
;;
;; Title:       TraditionalBridgeController.dbl
;;
;; Description: WebAPI controller to expose example Traditional Bridge Routines
;;
;;*****************************************************************************

import Microsoft.AspNetCore.Authorization
import Microsoft.AspNetCore.Mvc
import Microsoft.Extensions.Configuration
import Microsoft.Extensions.Options
import Newtonsoft.Json
import System
import System.Collections.Generic
import System.Linq
import System.Text
import System.Threading.Tasks

import Services.Controllers
import Services.Models

namespace Services.Controllers

    {Authorize}
    {Route("TraditionalBridge")}
    public partial class TraditionalBridgeController extends ControllerBase

        ;; Services provided via dependency injection
        private _TraditionalBridgeService, @TraditionalBridgeService

        public method TraditionalBridgeController
            aTraditionalBridgeService, @TraditionalBridgeService
        proc
            _TraditionalBridgeService = aTraditionalBridgeService
        endmethod

        {Route("AddTwoNumbers/{aNumber1}/{aNumber2}")}
        public async method GetAddTwoNumbers, @Task<IActionResult>
            required in aNumber1, decimal
            required in aNumber2, decimal
        proc
            mreturn ok(await _TraditionalBridgeService.AddTwoNumbers(aNumber1,aNumber2))
        endmethod

    endclass

endnamespace

As you can see, the controller class inherits from ControllerBase, making it a Web API controller, and the constructor method requests a TraditionalBridgeService object from the dependency injection container, saving a reference to it in a class variable _TraditionalBridgeService.

Notice the {Route("TraditionalBridge")} attribute on the controller class, meaning that the controllers endpoints can be accessed at the base address of the service, with the additional path /TraditionalBridgeService.

Then an endpoint method is added, one for each exposed routine, and the job of each of the endpoint methods is to obtain the necessary inbound parameters and pass them along to the associated wrapper method in the service object. Again, the outbound information (just one thing in this case) is passed along with the HTTP response. on this case the code ok(await _TraditionalBridgeService.AddTwoNumbers(aNumber1,aNumber2)) generates an HTTP 200 (OK) response, containing the value that is extracted (by the await statement) from the Task returned by the wrapper method in the service object.

Special Processing Extensibility Points

In Harmony Core V??.?? we added support for extensibility points that allow you to execute custom code before or after your .NET service class calls over to a Traditional Bridge method, or before or after your traditional Synergy master dispatcher class calls into a specific routine dispatcher class. In both cases we refer to these extensibility points as BeforeCall and AfterCall.

An example use case is to pass context or authorization information over the wire from Harmony Core to Traditional Bridge. For example, you could use a BeforeCall method on the .NET side to embed user identification or authorization information into the Traditional Bridge call, as one or more additional parameters, and you could then use a BeforeCall method on the traditional Synergy side to extract that information and action it in some way, perhaps validating that the user has access, or setting some custom context based on the additional information passed along with the call.

BeforeCall and AfterCall Methods on the .NET Side

On the .NET side, the BeforeCall and AfterCall methods are added to your service class, the class that inherits from DynamicCallProvider. Here is an example:

;;*****************************************************************************
;;
;; Title:       TraditionalBridgeService.dbl
;;
;; Description: Exposes example traditional Synergy routines via Traditional
;;              Bridge JSON-RPC calls.
;;
;;*****************************************************************************

import Harmony.Core
import Harmony.Core.Context
import Harmony.Core.Interface
import Harmony.Core.EF.Extensions
import Harmony.OData
import Harmony.OData.Adapter
import System.Threading.Tasks
import System
import System.Collections.Generic
import System.Text
import Services
import Services.Models
import Microsoft.AspNetCore.Mvc
import Microsoft.AspNet.OData
import Microsoft.AspNetCore.Authorization
import Newtonsoft.Json.Linq
import System.Linq

import Services.Controllers
import Services.Models

namespace Services.Controllers

    public partial class TraditionalBridgeService extends DynamicCallProvider

        ;;; <summary>
        ;;; Constructor
        ;;; </summary>
        public method TraditionalBridgeService
            connection, @IDynamicCallConnection
            parent(connection)
        proc
         
        endmethod

        ;;; <summary>
        ;;; Called before each bridge call. You can alter or add parameters here.
        ;;; </summary>
        ;;; <param name="name">Name of the method being called</param>
        ;;; <param name="args">Arguments being passed to the method, which you can alter!</param>
        ;;; <returns>The arguments to be passed to the Traditional Bridge call</returns>
        protected internal override method BeforeCall, [#]@Object
            name, string
            args, [#]@Object
        proc
            ;; Copy the array of arguments (so we're not changing anything inside Harmony Core)
            data authedArgs = args
            ;; Add an additional "pretend" argument
            Array.Resize(authedArgs, args.Length + 1)
            ;;And populate it with the current users name
            authedArgs[args.Length + 1] = contextAccessor.ActionContext.HttpContext.User.Identity.Name
            ;;And use these parameters for the Traditional Bridge call
            mreturn authedArgs
        endmethod

;        ;;; <summary>
;        ;;; Called after each bridge call, before returning to your code
;        ;;; </summary>
;        ;;; <param name="name">Name of the method that was called</param>
;        ;;; <param name="result">Results from the method that yoo could change!</param>
;        ;;; <returns></returns>
;        protected internal virtual method AfterCall<T>, @Tuple<T, [#]@Object>
;            name, @string
;            result, @Tuple<T, [#]@Object>
;        proc
;            ;;Not sure of a use case, but you can override AfterCall and it will be called
;            ;;after each bridge call, before control returns to your code.
;            mreturn result
;        endmethod

        ;;; <summary>
        ;;; Routine wrapper method for xcall AddTwoNumbers
        ;;; </summary>
        ;;; <parameter name="aNumber1">First number</param>
        ;;; <parameter name="aNumber2">Second number</param>
        ;;; <returns>The sum of the two numbers</returns>
        public async method AddTwoNumbers, @Task<decimal>
            required in aNumber1, decimal
            required in aNumber2, decimal
        proc
            ;; Prepare a variable indicating the type of the returned third parameter
            data sum, decimal

            ;; Call the method passing parameters and return value type
            data resultTuple = await CallMethod("AddTwoNumbers",aNumber1,aNumber2,sum)

            ;; Extract the result from parameter 3
            sum = ArgumentHelper.Argument<decimal>(3,resultTuple)

            ;; And return it
            mreturn sum 

        endmethod

    endclass

endnamespace

BeforeCall and AfterCall Methods on the Traditional Side

;;*****************************************************************************
;;
;; Title:       MethodDispatcher.dbl
;;
;; Description: Declares dispacher classes for exposed methods
;;
;;*****************************************************************************

import Harmony.TraditionalBridge

namespace TraditionalBridge.Dispatchers

    public partial class MethodDispatcher extends RoutineDispatcher

        public method MethodDispatcher
        proc
            ;;Register routine dispatcher classes 
            mDispatchStubs.Add("AddTwoNumbers",  new AddTwoNumbersDispatcher())
        endmethod

        .include "SYS:sysparams.def"

        ;;; <summary>
        ;;; Called before each bridge call. You can alter or add parameters here.
        ;;; </summary>
        ;;; <param name="name">The name of the method being called</param>
        ;;; <param name="callFrame">Contains the arguments being passed to the method, which you can alter!</param>
        ;;; <param name="serializer">The @DispatchSerializer used to return information to the caller</param>
        public override method BeforeDispatch, void
            name, @string
            callFrame, JSON_ELEMENT
            serializer, @DispatchSerializer
            record
                arguments, JSON_ELEMENT
            endrecord
        proc
            ;; If the BeforeCall method on the .NET side added a 'virtual parameter' containing the user name
            ;; then we can extract that value from the last parameter in the call for use in the traditional
            ;; Synergy side. Here we access the last parameter as text and store the value in a common variable.
            arguments = callFrame.GetProperty("params")
            sysparams.user_name = dispatcher.GetText(arguments[arguments.GetArrayLength()-1])
        endmethod

;        ;;; <summary>
;        ;;; Called before each bridge call. You can alter or add return parameters here.
;        ;;; </summary>
;        ;;; <param name="name">The name of the method being called</param>
;        ;;; <param name="callFrame">Contains the arguments being passed to the method, which you can alter!</param>
;        ;;; <param name="serializer">The @DispatchSerializer used to return information to the caller</param>
;        public override method AfterDispatch, void
;            name, @string
;            callFrame, JSON_ELEMENT
;            serializer, @DispatchSerializer
;        proc
;            ;; Not sure of a use case, but you can override AfterDispatch and it will be called
;            ;; after routine call, before control returns to your code.
;        endmethod

    endclass

endnamespace
Clone this wiki locally