-
Notifications
You must be signed in to change notification settings - Fork 14
Tutorial 04 08 Add Helper Service Class
For each traditional Synergy routine that you expose through Traditional Bridge, there needs to be a client-callable wrapper method on the .NET side of the Harmony Core solution. Ultimately, Synergy routines must be exposed as web service endpoints, which means they need to be exposed as methods in a controller class—a "helper service class"—in your Services.Controllers project.
The helper service class must inherit from the class Harmony.Core.Context.DynamicCallProvider
, which contains the types and code necessary to be a client to a Traditional Bridge host application. It marshals method calls into JSON-RPC requests, sends requests, receives and decodes responses, and hands appropriate response data back to calling routines.
The helper service class, BridgeMethodsService
, was created for us when we generated code after adding the BridgeMethods
interface. Now we need to add it to the solution:
-
In Visual Studio Solution Explorer, right-click Services.Controllers and select
Add > Existing Item
from the context menu. -
In the Add Existing Item dialog, navigate to the Services.Controllers folder, select
BridgeMethodsService.dbl
, and clickAdd
. -
Open
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 ofIDynamicCallConnection
, which represents a connection from Harmony Core to the Traditional Bridge host application and 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
NOTE: Further down in the code you'll probably see code marked with red squiggles. These squiggles will appear when the background build for IntelliSense is complete, andy they will disappear when we add the controller class (BridgeMethodsController.dbl
) and response model classes (BridgeMethodsServiceModels.dbl
). We'll add these in the next part of this tutorial (Exposing Endpoints for Traditional Bridge Routines).
As mentioned above, the helper service class must include a client-callable wrapper method for each routine exposed by your Traditional Bridge environment. Essentially, when code in a controller class calls a Traditional Bridge routine, it will obtain a copy of the helper service class via dependency injection and then call the appropriate wrapper method.
Before we examine the helper service class we just added to our solution, let's take a look at requirements for wrapper methods. Each wrapper in the helper service class must be a public async
method. Additionally, each wrapper must call the traditional Synergy routine and do the following:
-
Return an @Task or @Task value. Because an async pattern is being used in this class, all wrapper methods must return a
Task
object. If the underlying traditional Synergy routine is a subroutine, the return value of the wrapper method should be defined as@Task
. If the underlying traditional Synergy routine is a function, the return value of the wrapper method should be defined as@Task<T>
, with the generic typeT
being based on the return value type of the function. Again, appropriate mappings between traditional Synergy and .NET need to be used. - Include parameters required for the traditional Synergy routine. A wrapper method must have parameters that match the parameters of the underlying traditional Synergy routine.
- Translate argument types. Each method must 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 wrapper must translate each one. These translations are described in Parameters and Data Types below.
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. For example, a tuple might be a data structure with three elements: Item1
that's an integer, Item2
that's a decimal, and Item3
that's a string. Tuple elements are always named Item1
, Item2
, and so on.
It is 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 the first argument, 2 is the second argument, and so on.
-
The second parameter is the tuple returned by
CallMethod
.
Here is an example that uses the helper to extract the value of an alpha function return value:
data retval = ArgumentHelper.Argument<string>(0,returnTuple)
And the following uses the helper to extract a numeric value of a third parameter that is out
or inout
:
data param3value = ArgumentHelper.Argument<decimal>(3,returnTuple)
Each wrapper method in the helper service class must translate argument types from the .NET 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 wrapper must translate each one. These translations are described in the sections that follow:
Parameters 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
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. These can be generated by doing the following in the Harmony Core GUI tool:
-
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
andODataModel.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 Optional Parameters below), set the
Enable optional parameters
option on the Traditional Bridge screen in the Harmony Core GUI tool. -
You'll then need to save these changes by selecting (
File > Save
) and generate code for the solution (by selectingCodegen > Regen
from the menu).
Once the client-side data object classes have been generated, use these classes in the client-side wrapper 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
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
For optional parameters, Traditional Bridge supports the primitive types 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
Now let's look at the wrapper methods that were generated when we selected Cogegen > Regen
after adding the BridgeMethods
interface.
Remember that at this point some lines in the code will be marked with red squiggles. These squiggles will disappear when we add the controller class (BridgeMethodsController.dbl
) and response model classes (BridgeMethodsServiceModels.dbl
) in the next part of this tutorial (Exposing Endpoints for Traditional Bridge Routines).
-
Scroll down to the wrapper method for the
GetEnvironment
routine inBridgeMethodsService.dbl
:;;; <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
. The next statement extracts the first item from the tuple (the return value) and returns it to the calling routine.
-
Next, find the wrapper code for the
GetLogicalName
routine, which is very similar to the preceding example, except that it has an inbound parameter.;;; <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
-
Finally, take a look at the wrapper code for the
AddTwoNumbers
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, but here there are 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 value returned by CallMethod
. As in previous examples, out
parameters are encoded into the returned tuple.
Next topic: Exposing Endpoints for Traditional Bridge Routines
-
Tutorial 2: Building a Service from Scratch
- Creating a Basic Solution
- Enabling OData Support
- Configuring Self Hosting
- Entity Collection Endpoints
- API Documentation
- Single Entity Endpoints
- OData Query Support
- Alternate Key Endpoints
- Expanding Relations
- Postman Tests
- Supporting CRUD Operations
- Adding a Primary Key Factory
- Adding Create Endpoints
- Adding Upsert Endpoints
- Adding Patch Endpoints
- Adding Delete Endpoints
-
Harmony Core Code Generator
-
OData Aware Tools
-
Advanced Topics
- CLI Tool Customization
- Adapters
- API Versioning
- Authentication
- Authorization
- Collection Counts
- Customization File
- Custom Field Types
- Custom File Specs
- Custom Properties
- Customizing Generated Code
- Deploying to Linux
- Dynamic Call Protocol
- Environment Variables
- Field Security
- File I/O
- Improving AppSettings Processing
- Logging
- Optimistic Concurrency
- Multi-Tenancy
- Publishing in IIS
- Repeatable Unit Tests
- Stored Procedure Routing
- Suppressing OData Metadata
- Traditional Bridge
- Unit Testing
- EF Core Optimization
- Updating a Harmony Core Solution
- Updating to 3.1.90
- Creating a new Release
-
Background Information