Dynamic (Discovered) Service Brokers

Dynamic Service Brokers are used when we want to create a re-usable Broker that can expose different instances of the same technology. They are Dynamic because the broker will discover the Schema of the provider when a Service Instance of the Broker is registered or refreshed.

Here is an example: suppose you wanted to create a Service Broker that can interact with MySQL databases. The Broker will always interact with the same technology (MySQL), but we cannot predict what the underlying Schema of a particular instance of a MySQL database will look like. You therefore need to create a Dynamic Service Broker that will query a particular instance of a MySQL database when a Service Instance is registered or refreshed. The discovery process will use some kind of query to determine the various tables, views and procedures available in that MySQL database, and then describe these in Service Object terms.

Sample Project
You can download a sample Visual Studio project for a Dynamic Service Broker that exposes XML Files from here: K2Documentation.Samples.Extensions.DynamicServiceBroker (on GitHub). The project includes sample XML files that you can use to test this sample dynamic service broker with. The sample project is provided for demonstration purposes only. It is not supported by K2 product support and is not intended to be used as-is in production environments.

Implementing Dynamic Service Brokers

A Dynamic Service Broker is implemented very similarly to a Static Service broker, but with one key difference: instead of decorating the Classes, Properties and Methods that you want to add to your Service Broker, you will dynamically generate Service Objects and add Properties and Methods when the DescribeSchema() method is called.

1. Create .NET class library (or edit existing class)

Custom Brokers are standard .NET Class libraries (.dlls). If you have an existing class that you want to expose as a Service Broker, you can edit the code in your class and add the necessary implementation or alternatively reference your assembly in a new class project. If you are referencing a custom assembly, remember that you will need to copy the referenced assembly to the K2 server along with the Service Broker .dll file. (For a Dynamic Service Broker, the chances are good that you will need to reference an external assembly, so keep this dependency in mind when deploying.)

2. Add Reference to SourceCode.SmartObjects.Services.ServiceSDK.dll

Add a reference to the .NET library SourceCode.SmartObjects.Services.ServiceSDK.dll which is located by default in %PROGRAMFILES%\K2\Bin. This file is available on the K2 server and in a machine where the K2 design tools are installed. When deploying your project you don’t need to copy this assembly to the K2 server, since it already exists on the K2 server.

After adding the reference, import the SourceCode namespaces that you will need to use in your project:

Copy

SourceCode namespaces

using SourceCode.SmartObjects.Services.ServiceSDK;
using SourceCode.SmartObjects.Services.ServiceSDK.Objects;
using SourceCode.SmartObjects.Services.ServiceSDK.Types;

3. Inherit the base class: SourceCode.SmartObjects.Services.ServiceSDK.ServiceAssemblyBase

Next inherit the base class SourceCode.SmartObjects.Services.ServiceSDK.ServiceAssemblyBase in your project. We recommend creating a separate .cs class for the base class to separate the Broker code implementation from the class you will expose as a ServiceObject.

Copy

Inherit the base class

class CustomDynamicServiceBrokerClass : ServiceAssemblyBase

When you inherit this base class in your class, you will override three basic methods for your service broker: GetConfiguration, DescribeSchema and Execute.

Example: Importing namespaces and inheriting from the base class:

4. Define Configuration values in the GetConfiguration() method

The next step is to add any configuration settings to your broker. Dynamic Service Brokers usually need additional configuration settings, since you would normally be able to register the same Service Type against different instances of the same Provider technology.

You add configuration values by overriding and implementing the base class' GetConfigSection() method. For each configuration setting in your Service Broker, add it to the Service Configuration items. Examples below.

Copy

Add configuration values

public override string GetConfigSection()
{
    //In this example, we are adding two configuration values, one required and one optional, and one with a default value
    this.Service.ServiceConfiguration.Add("RequiredConfigurationValue", true, "RequiredValueDefaultValue");
    this.Service.ServiceConfiguration.Add("OptionalConfigurationValue", false, string.Empty);
    //return the configuration as XML
    return base.GetConfigSection();
}

You can add as many configuration settings as you like; just keep adding to the Service.ServiceConfiguration collection. At runtime, you can read the configuration values like this:

Copy

Reading the configuration values

string configSettingValue = this.Service.ServiceConfiguration["ConfigSettingName"].ToString();

Example: Implementing configuration settings:

5. Override and Implement the DescribeSchema() method

In a dynamic Service Broker, you will generate Service Objects dynamically in the DescribeSchema() method of the ServiceAssemblyBase base class. This method is called when you register or refresh a Service Instance, and it is used to list the available Service Objects for a Service Broker. Remember that you will manually register or refresh a Service Instance to call this method: K2 does not "poll" or otherwise automatically update the list of Service Objects in a Service Broker. This is especially important to know when your Providers Schema is dynamic: you may need to refresh your Service Instance from time to time to pick up changes to the Providers Schema.

K2 will not automatically update SmartObjects that use the Provider. If the Schema changes significantly, especially if Properties or Methods are changed or removed, there is a real possibility that existing SmartObjects can be broken. Generally, adding properties or methods is safe, but remember that you will need to update your SmartObject definitions manually if you want to take advantage of any new Properties or Methods in your Provider.

We recommend implementing the DescribeSchema in two separate methods. The override implementation of DescribeSchema() gathers configuration settings, sets up Type Mappings and sets up the Service Instance after the Service Objects have been discovered and added. The actual discovery of the Schema is performed by a helper method. The helper method will discover the Objects in the Provider and then add Service Objects for each object that we want to expose in the Service Broker.

Copy

Implementing the DescribeSchema

public override string DescribeSchema()
{
    //1. GET SERVICE INSTANCE CONFIGURATION VALUES

    //we usually need to retrieve the Service Instance configuration settings before calling the method that connects to the Provider
    string myConfigSetting = this.Service.ServiceConfiguration["ConfigSetting"].ToString();
    //2. SET UP SERVICE OBJECT TYPE MAPPINGS
    //set up the Service Object DataType Mappings for the service. This is a table which
    //tells the service discovery method how to map the native data types for the
    //Provider to equivalent Service Object data types
    TypeMappings map = new TypeMappings();
    // Add type mappings.
    map.Add("Int32", SoType.Number);
    map.Add("String", SoType.Text);
    map.Add("Boolean", SoType.YesNo);
    map.Add("Date", SoType.DateTime);
    // Add the type mappings to the Service Instance.
    this.Service.ServiceConfiguration.Add("Type Mappings", map);
    //3. DISCOVER THE SCHEMA OF THE UNDERLYING PROVIDER
    //here we will connect to the Provider and discover the schema.
    //During the discovery phase, we will add one or more ServiceObjects (SourceCode.SmartObjects.Services.ServiceSDK.Objects) to the Service Instance.
    //Each ServiceObject contains a collection of Properties of type
    //SourceCode.SmartObjects.Services.ServiceSDK.Objects.Property
    //and a collection of Methods of type
    //SourceCode.SmartObjects.Services.ServiceSDK.Objects.Method
    //see the DiscoverSchemaHelper method for an example of iterating over the objects in the Provider and adding service objects
    DiscoverSchemaHelper(myConfigSetting);
    //4. SET UP THE SERVICE INSTANCE (Optional)
    //Set up the default values for the Service Instance. The user will be able to override these values
    this.Service.Name = "ServiceInstanceName";
    this.Service.MetaData.DisplayName = "Service Instance Display Name";
    this.Service.MetaData.Description = "Service Instance Description";
    // Indicate that the operation was successful.
    ServicePackage.IsSuccessful = true;
}

Example: Implementing the DescribeSchema method and adding type mappings:

The actual discovery of the Schema is performed by a helper method. The helper method will discover the Objects in the Provider and then add Service Objects for each object that we want to expose in the Service Broker. In the example below, we are querying a .NET DataSet to determine the Service Objects that should be added. For a different provider, the discovery process may be different.

Copy

Example of querying a .NET DataSet to determine the Service Objects

public override string DescribeSchema()
{
    //1. GET SERVICE INSTANCE CONFIGURATION VALUES
    //we usually need to retrieve the Service Instance configuration settings before calling the method that connects to the Provider
    string myConfigSetting = this.Service.ServiceConfiguration["ConfigSetting"].ToString();

    //2. SET UP SERVICE OBJECT TYPE MAPPINGS
    //set up the Service Object DataType Mappings for the service. This is a table which
    //tells the service discovery method how to map the native data types for the
    //Provider to equivalent Service Object data types
    TypeMappings map = new TypeMappings();
    // Add type mappings.
    map.Add("Int32", SoType.Number);
    map.Add("String", SoType.Text);
    map.Add("Boolean", SoType.YesNo);
    map.Add("Date", SoType.DateTime);
    // Add the type mappings to the Service Instance.
    this.Service.ServiceConfiguration.Add("Type Mappings", map);

    //3. DISCOVER THE SCHEMA OF THE UNDERLYING PROVIDER
    //here we will connect to the Provider and discover the schema.
    //During the discovery phase, we will add one or more ServiceObjects (SourceCode.SmartObjects.Services.ServiceSDK.Objects) to the Service Instance.
    //Each ServiceObject contains a collection of Properties of type
    //SourceCode.SmartObjects.Services.ServiceSDK.Objects.Property
    //and a collection of Methods of type
    //SourceCode.SmartObjects.Services.ServiceSDK.Objects.Method
    //see the DiscoverSchemaHelper method for an example of iterating over the objects in the Provider and adding service objects
    DiscoverSchemaHelper(myConfigSetting);

    //4. SET UP THE SERVICE INSTANCE (Optional)
    //Set up the default values for the Service Instance. The user will be able to override these values
    this.Service.Name = "ServiceInstanceName";
    this.Service.MetaData.DisplayName = "Service Instance Display Name";
    this.Service.MetaData.Description = "Service Instance Description";
    // Indicate that the operation was successful.
    ServicePackage.IsSuccessful = true;
    return base.DescribeSchema();
}

private void DiscoverSchemaHelper(string configSetting)
{
    //read the type mappings so that we can convert the DataTable"s property types into
    //equivalent Service Object Property types
    TypeMappings map = (TypeMappings)this.Service.ServiceConfiguration["Type Mappings"];
    //in this example we will load the provider into a DataSet and then iterate over the dataset
    //to add the Service Objects, Properties and Methods
    try
    {
        //read the target XML file into a DataSet so we can discover it
        DataSet pseudoDataSource = new DataSet("PseudoDataSource");
        //load the dataset (method may be different for you)
        pseudoDataSource.Load(pseudoDataSource.CreateDataReader(), LoadOption.PreserveChanges, configSetting);
        //iterate through each DataTable in the DataSource
        foreach (DataTable table in pseudoDataSource.Tables)
        {
            //1. CREATE SERVICE OBJECTS
            //we will create a Service Object for each table in the DataSet
            ServiceObject svcObject = new ServiceObject();
            //clean up the System Name
            svcObject.Name = table.TableName.Replace(" ", "");
            svcObject.MetaData.DisplayName = table.TableName;
            //2. CREATE SERVICE OBJECT PROPERTIES
            //we will create Service Object Properties for each column in the Table
            foreach (DataColumn column in table.Columns)
            {
                //note that the Name cannot have spaces
                Property svcProperty = new Property(column.ColumnName.Replace(" ", ""));
                svcProperty.MetaData.DisplayName = column.ColumnName;
                //set the property type based on the type mappings defined for the service
                svcProperty.SoType = map[column.DataType.Name];
                svcObject.Properties.Create(svcProperty);
            }
            //3. CREATE SERVICE OBJECT METHODS
            //we will only create Read and List methods for the XML Service Broker
            //LIST Method.
            Method svcListMethod = new Method();
            //note that the Name should not contain spaces
            svcListMethod.Name = "List" + table.TableName.Replace(" ", "");
            svcListMethod.MetaData.DisplayName = "List " + table.TableName;
            svcListMethod.Type = MethodType.List;
            //Set up the return properties for the List Method
            ReturnProperties listReturnProperties = new ReturnProperties();
            //for this method we'll return each column as a property.
            foreach (Property svcProperty in svcObject.Properties)
            {
                listReturnProperties.Add(svcProperty);
            }
            svcListMethod.ReturnProperties = listReturnProperties;
            //Set up the input properties for the List Method
            InputProperties listInputProperties = new InputProperties();
            //for this method we'll return each column as a property.
            foreach (Property svcProperty in svcObject.Properties)
            {
                listInputProperties.Add(svcProperty);
            }
            svcListMethod.InputProperties = listInputProperties;
            svcObject.Methods.Create(svcListMethod);
            //READ Method
            Method svcReadMethod = new Method();
            //note that the Name should not contain spaces
            svcReadMethod.Name = "Read" + table.TableName.Replace(" ", "");
            svcReadMethod.MetaData.DisplayName = "Read " + table.TableName;
            svcReadMethod.Type = MethodType.Read;
            //Set up the return properties for the Read Method
            ReturnProperties readReturnProperties = new ReturnProperties();
            //for this method we'll return each column as a property.
            foreach (Property svcProperty in svcObject.Properties)
            {
                readReturnProperties.Add(svcProperty);
            }
            svcReadMethod.ReturnProperties = readReturnProperties;
            //Set up the input properties for the method
            InputProperties inputProperties = new InputProperties();
            //for this method we will define the first column in the datatable as the input property
            inputProperties.Add(svcObject.Properties[0]);
            svcReadMethod.InputProperties = inputProperties;
            //define the required properties for the method.
            //in this case, we will assume that the first property of the item is also the Key
            //value for the item which we will require to locate the specified item in the provider
            svcReadMethod.Validation.RequiredProperties.Add(inputProperties[0]);
            //now add the Read method to the Service Object
            svcObject.Methods.Create(svcReadMethod);
            //Use Method parameters if you want to define a parameter for the method
            //Parameters are used when the required input value is not already a Property of the Service Object.
            //if the required input value is already defined as a property for the SmartObject, use RequiredProperties instead
            MethodParameter readMethodParameter = new MethodParameter();
            readMethodParameter.Name = "SomeName";
            readMethodParameter.MetaData.DisplayName = "SomeDisplayName";
            //readMethodParameter.SoType = [one of the available SO Types];
            svcReadMethod.MethodParameters.Create(readMethodParameter);
            //4. ADD THE SERVICE OBJECT TO THE SERVICE INSTANCE
            // Activate Service Object for use, otherwise you cannot create SmartObjects for the service object
            svcObject.Active = true;
            this.Service.ServiceObjects.Create(svcObject);
        }
    }
    catch
    {
        throw;
    }
}

Example: Adding Service Objects:

Example: Adding Service Object Properties:

Example: Adding Service Object Methods:

Example: Adding the generated Service object to the ServiceObjects collection:

6. Override and Implement the Execute() method

Finally, override and implement the Execute() method in the ServiceAssemblyBase base class. This method is called at runtime whenever a SmartObject interacts with your Service Broker. Note that, regardless of the method called this is always the entry point into your Broker, so you will need to examine the request to determine which Service Object and Method was requested, and what the input properties, parameters and return properties are. You will then usually call out to helper methods to perform the interaction with the Provider.

Copy

Override and Implement the Execute() method

public override void Execute()
{
    //1. GET SERVICE INSTANCE CONFIGURATION SETTING
    string myConfigSetting = this.Service.ServiceConfiguration["ConfigSettingName"].ToString();

    //2. GET THE SERVICE OBJECT THAT WAS REQUESTED
    //at runtime, the requested Service Object is always in the [0] position of the Service.ServiceObjects array
    ServiceObject serviceObject = Service.ServiceObjects[0];

    //3. GET THE METHOD THAT WAS REQUESTED
    //at runtime, the requested Service Object Method is always in the [0] position of
    //the ServiceObject.Methods array
    Method method = serviceObject.Methods[0];

    //4. GET THE INPUT PROPERTIES AND RETURN PROPERTIES FOR THE REQUESTED METHOD
    // InputProperties and ReturnProperties are string collections,
    // create property collections for later ease-of-use.
    Property[] inputs = new Property[method.InputProperties.Count];
    Property[] returns = new Property[method.ReturnProperties.Count];
    MethodParameter[] parameters = new MethodParameter[method.MethodParameters.Count];
    //populate the Input Properties collection
    for (int i = 0; i < method.InputProperties.Count; i++)
    {
        inputs[i] = serviceObject.Properties[method.InputProperties[i]];
    }
    //populate the return properties collection
    for (int i = 0; i < method.ReturnProperties.Count; i++)
    {
        returns[i] = serviceObject.Properties[method.ReturnProperties[i]];
    }
    //populate the method parameters collection
    for (int i = 0; i < method.MethodParameters.Count; i++)
    {
        parameters[i] = method.MethodParameters[i];
    }

    //OBTAINING THE SECURITY CREDENTIALS FOR A SERVICE INSTANCE
    //if you need to obtain the authentication credentials (username/password) for the
    //service instance, query the following properties:
    //Note: password may be blank unless you are using Static or SSO credentials
    string username = this.Service.ServiceConfiguration.ServiceAuthentication.UserName;
    string password = this.Service.ServiceConfiguration.ServiceAuthentication.Password;

    //5. EXECUTE THE ACTUAL METHOD
    //here we will call a helper method to do the actual execution of the method.
    //you will need to implement your own helper method
    //Execute(inputs, returns, method.Type, serviceObject, myConfigSetting);
    // Indicate that the operation was successful.
    ServicePackage.IsSuccessful = true;
}

Example: The Execute() method (Note: only the Read method is shown here):