Connecting to the Avid Platform using the Avid Connector API

The Avid Connector API connects to the Avid Platform over the Avid Secure Gateway. To connect your service to the Avid Platform, the Avid Secure Gateway must be running on a known host and port. The default port is 9900.

Default Connection Settings

If no special settings are passed in, and no special environment variables defined, the following defaults are used:

  • Query Timeout (ms): 10,000
  • Gateway Port: 9900
  • Gateway Host: 127.0.0.1
  • Bus Initial Connection Attempts: -1 (try forever)
  • Bus Initial Reconnection Attempts: -1 (try forever)

To connect to the Avid Platform, create a BusAccess object.

When connecting to the Avid Platform, an authentication provider may be specified using a client id and client secret (default authentication is based on IP address).

1
2
3
4
5
6
7
8
9
10
11
12
13
// Create a new BusAccess, with default connection settings and default authentication provider, and connect to the bus immediately.
using (var bus = new BusAccess())
{
// ...
}

// OR

// Create a new BusAccess, with an authentication provider, and connect to the bus immediately.
using (var bus = new BusAccess(new ClientAuthentication("620f8ca6-0b0e-11e6-b512-3e1d05defe78", "19928139-f7da-4e90-b551-ad6bdaa8d9ab"))) {
{
// ...
}

Non-default settings can be set by passing a ConnectionInfo object to the BusAccess constructor.

1
2
3
4
5
6
7
8
9
10
11
12
13
// Create a new BusAccess, with non-default connection settings and default authentication provider, and connect to the bus immediately.
using (var bus = new BusAccess((new ConnectionInfo { InitialAttempts = 5 }))) {
{
// ...
}

// OR

// Create a new BusAccess, with non-default connection settings and an authentication provider, and connect to the bus immediately.
using (var bus = new BusAccess((new ConnectionInfo { InitialAttempts = 5, AuthenticationProvider = new ClientAuthentication("620f8ca6-0b0e-11e6-b512-3e1d05defe78", "19928139-f7da-4e90-b551-ad6bdaa8d9ab") }))) {
{
// ...
}

Constructing a BusAccess object and connecting to the bus can be accomplished in two separate operations. All of the BusAccess constructors accept a boolean connectNow parameter. Passing false for this will delay connecting to the bus until the Connect method is called.

The Connect method will not return until it establishes a connection or until the supplied CancellationToken has been canceled (via CancellationTokenSource.Cancel):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
    CancellationTokenSource cancel = new CancellationTokenSource();

// ...

var bus = new BusAccess(new IpAuthenticationProvider(), service, false));

// ...

try
{
bus.Connect(cancel.Token);
}
catch(OperationCanceledException)
{
// Connect was canceled
}

Overriding Default Settings with Environment Variables

You can override the default connection settings by setting pre-defined environment variables. The table below shows some of the variables/keys you can set to affect the way the bus connects:

Environment Variable Description
ACS_BUS_QUERY_TIMEOUT The default timeout (in ms) for Avid Platform queries. Default is 10000 ms
ACS_GATEWAY_HOST Secure Gateway connection host. Default is 127.0.0.1
ACS_GATEWAY_PORT Secure Gateway connection port. Default is 9900
ACS_GATEWAY_UNSECURE_PORT Port for not secured connection. Default is 9966
ACS_GATEWAY_PROTOCOL_SEQUENCE Sequence of protocols (or just one protocol) separated by coma ',' in which Avid Connector API will try to establish connection to Secure Gateway. Default is 'wss'. Allowed protocols are 'wss' and 'ws'. Possible combinations are 'wss,ws', 'ws', 'wss' or 'ws,wss'.
ACS_PLATFORM_IDENTIFIER Unique node identifier, where service is running. Must be provided by target platform, i.e.AWS, OpenStack, etc. (Default 'unknown')
ACS_SERVICE_BUILD_NUMBER RPM version or any other version of the service (Default 'unknown')
ACS_ENVIRONMENT_IDENTIFIER Environment identifier is basically chef generalized identifier for any collection of nodes (Default 'unknown')
ACS_BUS_INITIAL_CONNECTION_ATTEMPTS The # of times to attempt the initial connection before failing (-1 means to try forever)
ACS_GATEWAY_CONNECTION_LOST_THRESHOLD Amount of time, after which connection to Gateway considered as broken if we didn't get ping from gateway. (Default 5000 ms)
ACS_BUS_RECONNECTION_ATTEMPTS The # of times to attempt reconnecting a broken connection (-1 means to try forever)
ACS_BUS_RECONNECTION_DELAY Delay in ms between connection attempts to Secure Gateway. This delay same both for initial connection attempts and reconnections attempts. Default is 1000 ms.
ACS_SECURITY_TRUST_SELF_SIGNED Whether to trust (true) or not trust (false) to self signed certificates. Default is true.

Debugging the Secure Gateway Connection

The Avid Connector API and the Secure Gateway have an internal failover logic to validate the connection between them. If you are debugging your service in an IDE or on the command line, a breakpoint can block your service from receiving the information it needs to know that it is still connected to the Secure Gateway. To prevent the service from thinking it has lost its connection to the Secure Gateway, and thus failing over into reconnection mode, you should set the variable ACS_GATEWAY_CONNECTION_LOST_THRESHOLD=600000 (10 minutes). This should prevent the service from thinking it has lost connection while giving you enough time to inspect the information you need at the breakpoint.

1
2
3
4
5
6
7
8
9
static void Main()
{
#if (DEBUG)
Environment.SetEnvironmentVariable("ACS_BUS_QUERY_TIMEOUT", "-1");
Environment.SetEnvironmentVariable("ACS_GATEWAY_CONNECTION_LOST_THRESHOLD", "-1");
#endif

// ...
}

Connection Handler

If your application needs to react when a connection to the Avid Platform established/lost, pass in an implementation of an IConnectionSubscriber interface.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
namespace Avid.Platform.Bus
{
public interface IConnectionSubscriber
{
void OnConnect();
void OnDisconnect();
}
}

// ...
public class CalculatorService : StandaloneService, IChannelMessageSubscriber, IConnectionSubscriber
{
// ...
}

static void Main()
{
var service = new CalculatorService();
using (var bus = new BusAccess(new ConnectionInfo { Timeout = 2000, AuthenticationProvider = new ClientAuthentication("620f8ca6-0b0e-11e6-b512-3e1d05defe78", "19928139-f7da-4e90-b551-ad6bdaa8d9ab") }, service))
{
// ...
}
}

Threading Model

The Avid Connector API for .NET uses the .NET Framework's managed thread pool for all message processing e.g. service operations/requests, responses to query operations and channel messages.

There is only one thread pool per process.

Using the Avid Connector API as a Client

The Connector API can be used to send requests to services, and to subscribe and publish to channels. Requests and responses use the Message interface.

Providing Message Options

With each message operation (e.g. query, send, broadcast) you may provide message options, via a MessageOptions object. The following options are available:

  • Timeout - Specifies a message timeout in ms for query operations. Default is 10000ms.
  • Durable - Sets whether message is durable. Default is false. NOTE: This option is currently not implemented, and will be revised in the future releases.
  • AnyCompatibleVersion - Sets whether the message is delivered to any compatible version of the service or to an exact version of the service. Default is true.

Querying Services

All queries to services should be performed asynchronously. To execute an asynchronous query, pass an implementation of IAsyncQueryResult. The API contains a helper class that implements IAsyncQueryResult via delegates.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
    var msg = new Message("avid.acs.calculator", "global.test", 3, "echo");
bus.Query(msg, new AnonAsyncQueryResult() {
onSuccess = (Message result) => {
if (result.HasErrors) {
// ...
return;
}
Console.WriteLine("Echo reply: " + result + ".");
},
onError = (OperationError error) => {
Console.WriteLine("Failed to query service: " + msg.ServiceType + ", error: " + error.Message + ".");
},
onTimeout = () => {
Console.WriteLine("Timed-out querying service: " + msg.ServiceType + ".");
}
});
// ...

The onError delegate is only called when there are OperationError-type errors. The Message passed to onSuccess() will contain any errors that occurred while executing the service operation.

Sending to Services

Sending to a service is a one-way communication from the client to the service. There is no response. In comparison to a broadcast (described below), the client is also guaranteed that no more than one service instance will process the message.

1
2
3
4
5
6
7
8
// ...
var request = new Message("registry", "global", 0, "updateServiceStatus");
request.Parameters.Add("status", "ok");
bus.Send(request, new AnonAsyncOpResult() {
onError = (OperationError error) => {
Console.WriteLine("Failed to send message to service: " + request.ServiceType + ", error: " + error.Message + ".");
}
});

Broadcasting to Services

Broadcasting to a service is a one-way communication from the client to all instances of a given service. There is no response. As opposed to a send, the message is processed by every available instance of the service.

1
2
3
4
5
6
7
// ...
var request = new Message("widget.factory", "global", 0, "makeLotsAndLotsOfWidgets");
bus.Broadcast(request, new AnonAsyncOpResult() {
onError = (OperationError error) => {
Console.WriteLine("Failed to broadcast message to all service instance(s): " + request.ServiceType + ", error: " + error.Message + ".");
}
});

Remote Zone and Multi-Zone Communications

The default behavior of the Avid Connector API is to communicate within its own local zone. All the examples provided above use this default behavior. If the local zone has been initialized in a multi-zone environment, however, it is possible communicate with services in other zones.

Zone-Specific Communications

To communicate with a specific remote zone, use the bus.Zone(String zoneID) object. The following are examples of communications with other zones:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// ...
var msg = new Message("avid.acs.calculator", "global.test", 3, "add");
msg.Parameters.Add("num1", 2);
msg.Parameters.Add("num2", 2);

bus.Zone("5b2123f1-3f8e-4fcb-9263-f7b98bbdab0c").Query(msg, new AnonAsyncQueryResult() {
onSuccess = (Message result) => {
int sum = result.Results.GetItem<int>("sum");
Console.WriteLine("Sum = " + sum + ".");
},
onError = (OperationError error) => {
Console.WriteLine("Failed to query service: " + msg.ServiceType + ", error: " + error.Message + ".");
},
onTimeout = () => {
Console.WriteLine("Timed-out querying service: " + msg.ServiceType + ".");
}
});

// ...
var request = new Message("example.service", "global", 0, "doSomething");
bus.Zone("5b2123f1-3f8e-4fcb-9263-f7b98bbdab0c").Query(request, /* ... */);
bus.Zone("5b2123f1-3f8e-4fcb-9263-f7b98bbdab0c").Send(request, /* ... */);
bus.Zone("5b2123f1-3f8e-4fcb-9263-f7b98bbdab0c").Broadcast(request, /* ... */);

In all of the above cases, only services in the remote zone with an ID of 5b2123f1-3f8e-4fcb-9263-f7b98bbdab0c are invoked. In addition, only service instances registered with a scope of multi-zone are considered.

Multi-Zone Communications

To communicate across multiple zones, use the bus.MultiZone object. The following are examples of multi-zone communications:

1
2
3
4
5
var request = new Message("example.service", "global", 1, "doSomething");
bus.MultiZone.Query(request, /* ... */);
bus.MultiZone.Send(request, /* ... */);
bus.MultiZone.Broadcast(request, /* ... */);
`

Note that bus.MultiZone.Query and bus.MultiZone.Send only send to one service instance in one zone. If there is a service instance in the local zone, it sends to that one. Otherwise it sends to a service instance in a remote zone (if one is available and registered with the multi-zone scope). This is particularly useful if you know the service is in a zone, but aren't sure which one.

bus.MultiZone.Broadcast broadcasts to all matching service instances in all zones.

Local Zone Communications

Note that there is also a bus.LocalZone object. Invoking Query, Send, and Broadcast on this object is functionally equivalent to invoking the same methods on the base bus object.

Wildcards Usage in the Realm

If multiple realms of the same service are registered, you can use wildcards to address any service instance satisfying a wildcard expression. Wildcards can substitute for any letters/digits between the dot delimiters.

For example, consider an Avid Platform service registered with the following realms:

montreal.workgroup1;
montreal.workgroup2;
montreal.workgroup1.id1;
montreal.workgroup1.id2;
montreal.workgroup2.id1;
montreal.workgroup2.id2;

In the above case, you can address the services by supplying the following wildcarded realms in the request:

request = new Message("example.service", "*.*", 0, "echo");
request = new Message("example.service", "*.*.*", 0, "echo");
request = new Message("example.service", "montreal.*", 0, "echo");
request = new Message("example.service", "montreal.workgroup1.id*", 0, "echo");
request = new Message("example.service", "montreal.workgroup*.id2", 0, "echo");
request = new Message("example.service", "montreal.*group*.id2", 0, "echo");

// etc...

In contrast, the following wildcarded realms won't match any instance in the above example:

*
*.*.*.*
montreal.workgroup*.id3

// etc...

Proxy Clients

If you have the class (or an abstract class) definition of the service that you wish to call, the Avid Connector API provides a convenient way of interacting with the service. Using .NET Remoting, you can call the service as if it was a local object. Proxy clients can use the asynchronous API, if the service operation is declared as asynchronous (i.e. has an IResponder parameter).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var proxyService = Proxy.Async.Create<Calculator>(AppStateHolder.GetBusAccessClient(zone), "global.test");
var clientOperationContext = new ClientOperationContext<int>
{
onResponse = (res, resultMessage) =>
{
System.Console.WriteLine($"Result is: {res}");
},

onErrors = (errors, sourceMessage) =>
{
System.Console.WriteLine($"Errors occurred: {errors}");
System.Console.WriteLine($"Source message: {message}");
},

onTimeout = (timeout, sourceMessage) =>
{
System.Console.WriteLine($"Timeout occurred: {timeout}ms");
System.Console.WriteLine($"Source message: {message}");
}
};

proxyService.Add(234, 432, clientOperationContext);

Using the Avid Connector API to Host Services

You can use the Avid Connector API to host .NET services on the Avid Platform.

Simple Services

The easiest way to host a service on the Avid Platform is to derive an object from StandaloneService and annotate it with the Service attribute. To expose a method in your class as an operation available on the Avid Platform, annotate the method with the Operation attribute, as illustrated in the following example:

Note: In versions of the API less than 2.5, the IResponder interface was used to respond to operations (rather than IOperationContext).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
[Service("avid.platform.bus.examples.service.calculator", 1, "Performs simple arithmetic.", new int[] { 0 })]
[Examples("com.widget.Resources.Examples")]
[Error("DIVIDE_BY_ZERO", 400, ErrorSeverity.Error, "Division by zero, %{num1} and %{num2}.")]
class CalculatorService : StandaloneService
{
[Operation("add", "Returns the sum of two numbers.")]
[RestRequest("calculator/add")]
[return: Result("sum")]
public void Add(int a, int b, IOperationContext<int> responder)
{
responder.Respond(a + b);
}

[Operation("subtract", "Returns the difference of two numbers.")]
[RestRequest("calculator/subtract")]
[return: Result("difference")]
public void Subtract(int a, int b, IOperationContext<int> responder)
{
responder.Respond( a - b);
}

[Operation("multiply", "Returns the product of two numbers.")]
[RestRequest("calculator/multiply")]
[return: Result("product")]
public void Multiply(int a, int b, IOperationContext<int> responder)
{
responder.Respond( a * b);
}

[Operation("divide", "Returns the quotient of two numbers.")]
[RestRequest("calculator/divide")]
[return: Result("quotient")]
public void Divide(int a, int b, IOperationContext<int> responder)
{
if (b == 0) {
responder.Error(new Error("DIVIDE_BY_ZERO", new Dictionary<string, string> { { "num1", a.ToString() }, { "num2", b.ToString() } }));
return;
}
responder.Respond( a / b);
}
}

Note that the last parameter passed to the Service attribute declares that the service with the given version can handle requests targeted to other compatible versions. In this case, version 1 of the avid.platform.bus.examples.service.calculator service can also handle requests targeted to version 0.

The following code snippet illustrates how to create a connection to the Avid Platform, and register an instance of the service on it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static void Main()
{
var service = new CalculatorService();
using (var bus = new BusAccess(new ConnectionInfo { Timeout = -1 }, service))
{
// Create and register a calculator service (asynchronously):
bus.RegisterService(service,
new ServiceOptions { RequestServiceConfiguration = false },
new AnonAsyncOpResult<Avid.Platform.Bus.Service.ServiceBase>() {
onSuccess = (Avid.Platform.Bus.Service.ServiceBase result) => {
eventLog.WriteEntry("Registered service: " + result + ".");
},
onError = (OperationError error) => {
eventLog.WriteEntry("Failed to register service: " + service + ", error: " + error.Message + ".");
ExitCode = 3;
}
});

// Process messages
service.WaitForStop();
}
}

The ServiceOptions class also has a StartSuspended property, which will register the service in ServiceStatus.Suspended mode (a Suspended status indicates that the service is visible on the platform but will not receive any requests). It is expected that after registering in suspended mode, the service will eventually transition to an Ok status (typically during the processing of ServiceBase.OnServiceConfiguration or ServiceBase.OnRegistered).

Since StandaloneService derives from ServiceBase, note the following StandaloneService useful properties:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
namespace Avid.Platform.Bus.Service
{
[Examples("Avid.Platform.Bus.Resources.CommonOperations")]
public class ServiceBase : MarshalByRefObject
{
protected ServiceBase();
protected ServiceBase(string realm);

public IBusAccess Bus { get; internal set; }
public IDataSet Config { get; }
public ServiceInfo Info { get; }
public ServiceStatus Status { get; set; }

public virtual ServiceStatus OnRegistered();
public virtual void OnServiceConfiguration(ServiceConfigurationEventArg configArg);
public virtual void OnUnregistered();

public override string ToString();
}
}

Structured Errors

A service must declare its complete list of possible error codes via the Error attribute. The Error attribute constructor accepts many parameters.

1
public ErrorAttribute(string code, int status, ErrorSeverity severity, string messageTemplate, string description);
  • code is part of the service's public API and therefore the format of the code should be a short sentence in uppercase with an underscore symbol used as a separator, e.g. BAD_REQUEST, BULK_DUPLICATION, MISSING_ARGUMENT, QUOTA_EXCEEDED.
  • status parameter is the appropriate corresponding HTTP error code.
  • messageTemplate is text in en_US locale, that may include %{identifier} placeholders for error message parameters. The messageTemplate (and any parameters) should convey meaningful information e.g. "Quota on %{resourceName} exceeded for %{projectName}."

An error response to an operation is done by passing an Error object or an IErrorSet to the IOperationContext::Error method.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[Service("avid.platform.bus.examples.service.calculator", 1, "Performs simple arithmetic.", new int[] { 0 } )]
[Error("ADDITION_OVERFLOW", 500, ErrorSeverity.Error, "Addition overflow %{num1} and %{num2}.")]
[Error("DIVIDE_BY_ZERO", 400, ErrorSeverity.Error, "Division by zero, %{num1} and %{num2}.")]
public class ExampleCalculatorService : StandaloneService
{
// ...
[Operation("divide", "Returns the quotient of two numbers.")]
[RestRequest("calculator/divide")]
[return: Result("quotient")]
public void Divide(int a, int b, IOperationContext<int> responder)
{
if (b == 0) {
responder.Error(new Error("DIVIDE_BY_ZERO", new Dictionary<string, string> { { "num1", a.ToString() }, { "num2", b.ToString() } }));
return;
}
responder.Respond( a / b);
}

// ...
}

The description parameter may contain an elaborate description of an issue for internal use only that will only appear in error logs.

Exceptions can also be used to send an error response. The API will automatically transform a BusServiceException or any exception that contains API specific key/value pairs in the Exception.Data property (keyed using the strings listed in the static ExceptionKeys class) into an Error object and call IOperationContext.Error. For example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
using Avid.Platform.Bus.Exceptions;

public class OverflowException : Exception
{
public OverflowException(String code, long num1, long num2)
: base("Addition overflow")
{
Data.Add(ExceptionKeys.ErrorCode, "ADDITION_OVERFLOW");
Data.Add(ExceptionKeys.ErrorParameters, new Dictionary<string, string> { { "num1", num1.ToString() }, { "num2", num2.ToString() } });
}
}
// ...

public void Add(int num1, int num2, IOperationContext<int> responder)
{
if (AdditionWillOverflow(num1, num2))
throw new OverflowException("ADDITION_OVERFLOW", num1, num2);
responder.Respond(num1 + num2);
}

Multi-Zone Services

The default behavior of the Avid Connector API is to register services in the local zone scope. This means that by default services only receive requests from clients within the same zone. If the local zone is initialized in a multi-zone environment, however, it is possible to register a service in the multi-zone scope. This allows the service to be invoked by clients in any connected zone.

To register a service in the multi-zone scope, use the bus.MultiZone object:

1
2
3
4
5
6
7
8
bus.MultiZone.RegisterService(new CalculatorService(), new AnonAsyncOpResult() {
onSuccess = () => {
Console.WriteLine("Register service succeeded.");
},
onError = (OperationError error) => {
Console.WriteLine("Failed to register service, error: " + error.Message + ".");
}
});

Local Zone Scope

Registering a service using the bus.LocalZone object is functionally equivalent to registering it using the base bus object. The service is only accessible within the local zone.

Attributes

This section presents the attributes you can associate with a service's class implementation.

Service Attributes

Attribute Type Description
Service Indicates the service type, version, and description.
Examples Specifies example arguments for operations. You can provide examples by creating a new embedded resource for your project and specifying the parameter set for each operation.
Error Specifies any error the service may produce.

Operation Attributes

Attribute Type Description
Operation Exposes service operations.
RestRequest Annotates methods to expose a specific name as a REST request.

Return Value Attributes

Use the Result attribute to annotate a method's return value, to expose a specific name as part of a service API.

Receiving or Returning the Entire Message Object in the Operation

Most of the time service operations accept and return simple types or objects, but a service method may need to analyze the entire Message object, or need access to a parameter that is not easily de-serialized into a .NET type, or simply wants to return an entire Message.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Person
{
public String FirstName { set; get; }
public String LastName { set; get; }
}

// ...

[Operation("echoPersonInMessage", "Echo's back passed person.")]
[RestRequest("echoer/echoPersonInMessage")]
[return: Result("person")]
public void EchoPersonInMessage(Person person, IOperationContext<Message> responder)
{
var response = new Message(this, "echoPersonInMessage");
response.Results.Add("person", person);
responder.Respond(response);
}

The above code snippet could be used to yield result set similar to the following:

"resultSet": {
    "person": {
        "serviceType": "echoer,
        "serviceRealm": "global",
        "serviceVersion": 4,
        "op": "echoPersonInMessage",
        "resultSet": {
            "person": {
                "FirstName": "Tom",
                "LastName": "Brady"
            }
        }
    }
}

Note that eliminating the [return: Result("person")] attribute, yields a very different result set:

"resultSet": {
    "person": {
        "FirstName": "Tom",
        "LastName": "Brady"
    }
}

Avid Platform Service Events

Your service can indicate an interest in OnRegistered/OnUnregistered notifications, by re-implementing the corresponding methods in ServiceBase. In addition, there are IChannelMessageSubscriber and IConnectionSubscriber for channel and connection-related "events".

Service Configuration Changes

In order to receive OnServiceConfiguration notifications, a value of true must be set in the ServiceOptions.RequestServiceConfiguration property that is passed to the RegisterService method.

When overriding ServiceBase.OnServiceConfiguration it is a best practice to always set the status of the service based on whether the processing of the configuration was successful.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public override void OnServiceConfiguration(ServiceConfigurationEventArg configArg)
{
ServiceStatus status = ServiceStatus.Suspended;
try {
var config = configArg.Configuration;
// ...

status = ServiceStatus.Ok;
} catch (BusAccessException ex) {
Console.WriteLine("Failed to fetch service configuration, error: " + ex.Message + ".");
} catch (Exception ex) {
Console.WriteLine("Failed to process service configuration, error: " + ex.Message + ".");
} finally {
SetStatus(status, new AnonAsyncOpResult() {
onError = (OperationError error) => {
Console.WriteLine("Failed to set service status, error: " + error.Message + ".");
Stop();
}
});
}
}

When a service's configuration changes, the OnServiceConfiguration method is called. The current configuration is passed as an IDataSet.

Providing Service Status

When a service is registered, it starts sending heartbeats to report its current status (in the background). The reported status contains a status code.

By default, as long as the process is still running, the Avid Connector API reports an "OK" status. While this may be acceptable for simple services (like our calculator example), more complex services can explicitly supply their own status information. The .NET Connector API supports 5 status statues (OK, WARNING, ERROR, SUSPENDED, OFFLINE). Each state has its own behavior which define whether the service is still visible on the platform, and if it is able to recieve messages. See the table below to see the behavior defined by each status.

Status Visible on Platform Receives Requests Example Use Case
OK Yes Yes Service is fully functional (default state)
WARNING Yes Yes Service is still fully functioning, but want to warn about some resource (ie. high memory useage, large db latency, many timeouts to another service)
ERROR Yes No Service is not functioning properly, needs some intervention to fix (ie. ran out of memory, but wish to keep the process alive for debugging)
SUSPENDED Yes No Service is ok, but cannot function properly due to an external resource (ie. DB connection lost and service cannot proceed without persisting data)
OFFLINE No No Service process should stay alive, but should not be visible or routed to (ie. Keep service process alive during DB migration)

To set the status for a service, use the SetStatus method of ServiceBase.

1
2
3
4
5
6
7
8
9
10
11
12
13
transcodeService.SetStatus(ServiceStatus.Offline, new AnonAsyncOpResult() {
onError = (OperationError error) => {
Console.WriteLine("Failed to set status, error: " + error.Message + ".");
}
});

// ...

transcodeService.SetStatus(ServiceStatus.Ok, new AnonAsyncOpResult() {
onError = (OperationError error) => {
Console.WriteLine("Failed to set status, error: " + error.Message + ".");
}
});

Providing Custom Service Health Information

One of the core operations provided by the Avid Connector API is serviceHealth. By default if service receives this operation request it replies back with such message in the resultSet:

1
2
3
4
5
6
7
{
"service": {
"instanceId": "955742c3-1fdd-4edd-be71-78aeb837aaa0",
"healthStatus": "ok",
"healthVerifier": "default"
}
}

Service developers may override the serviceHealth operation by providing a specific healthStatus or additional customHealthInfo. In this case serviceHealth response may look like:

1
2
3
4
5
6
7
8
9
10
11
{
"service": {
"customHealthInfo": {
"str": "some string data",
"flag": true
},
"instanceId": "955742c3-1fdd-4edd-be71-78aeb837aaa0",
"healthStatus": "error",
"healthVerifier": "custom"
}
}

A service may choose to override the implementation of the serviceHealth operation by annotating a method with the HealthCheck attribute that accepts either a IHealthCheckReporter or IHealthCheckReporter<T> parameter:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
[HealthCheck]
internal void serviceHealth(IHealthCheckReporter<CustomHealthInfo> healthCheckReporter)
{
var info = new CustomHealthInfo("some text", true);
healthCheckReporter.Report(HealthStatus.Ok, info);
}

[DataContract]
public sealed class CustomHealthInfo {
[DataMember(Name = "str")]
public string Str {
get; private set;
}

[DataMember(Name = "flag")]
public bool Flag {
get; private set;
}

[JsonConstructor]
internal CustomHealthInfo(String str, bool flag)
{
Str = str;
Flag = flag;
}
}

In this case the serviceHealth response would look like:

1
2
3
4
5
6
7
8
9
"resultSet": {
"instanceId": "...",
"healthStatus": "ok",
"healthVerifier": "custom",
"customHealthInfo": {
"str": "some text",
"flag": true
}
}

Providing Compatible Version

Services on the Avid platform provide the concept of 'Compatible Versions' to provide a way to remain backwards compatibility with older versions of your service. When you declare that a newer version of your service is compatible with previous versions, it gains the ability to receive messages which were sent to an older version. This way clients who are sending messages to an older version of your service, will get a valid response, even if there are no instances of the old service running. For example, if we declare a new version of our service to be version == 3, but we also declare that it is compatible with versions 2 & 1, then any message which is sent to version 1, 2, or 3 of your service will be routed to version 3 if there is no running instance of your service in the specified version. To declare your service as compatible with other versions we add a comaptibleVersions array as the last parameter to the ServiceAttribute.

1
2
3
4
5
6
7
[Service("avid.platform.bus.examples.service.calculator", 3, "Performs simple arithmetic.", new int[] { 2, 1 })]
[Examples("com.widget.Resources.Examples")]
[Error("DIVIDE_BY_ZERO", 400, ErrorSeverity.Error, "Division by zero, %{num1} and %{num2}.")]
class CalculatorService : StandaloneService
{
// Service logic and operations...
}

Using the Avid Connector API for Channels

Channels are analogous to Java Message Service (JMS) API Topics. When a message is posted to a channel, it is broadcast as a one-way communication to all the subscribers listening to that channel. It is important to note that channel messages are not persisted, and if you post to a channel that has no subscribers, it will not result in an error.

Channels are identified by their name. Subscribers interested in listening on a channel must either receive the channel name from the service that owns the channel, or use a predefined or well-known channel name.

Note: The names of channels should be namespaced and end with a past-tense verb. The reason for using past tense is, generally speaking, channel messages convey "facts" - i.e. notifications of something that has already happened. For example, avid.protools.project.status.changed.

Posting to a Channel

Channel messages are very similar to regular Avid Platform messages, but rather than have Parameters or Results, they simply have Data. The IBusAccess API provides a method for posting to a channel:

1
2
3
4
5
6
7
8
9
var channelMessage = new ChannelMessage(channelName, subject);
channelMessage.Data.Add("event", "ingest_started");
channelMessage.Data.Add("mobId", "060a2b340101010101010f0013-000000-475d870b9a6b24fb-060e2b347f7f-2a80");
// ...
bus.PostToChannel(channelMessage, new AnonAsyncOpResult() {
onError = (OperationError error) => {
Console.WriteLine("Failed to post to channel: " + channelMessage.ChannelId + ", error: " + error.Message + ".");
}
});

NOTE: Once a channel is registered, anyone knowing the channel name can post to it. Although the poster is often the creator of the channel, this is not enforced in any way. This treatment is subject to change in a future edition of the API.

Subscribing to a Channel

Objects that need to subscribe to channels can do so by subscribing through the IBusAccess interface. First, a channel subscriber must implement the IChannelMessageSubscriber interface:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public interface IChannelMessageSubscriber
{
void OnChannelMessage(IChannelContext channelContext);
}

public interface IChannelContext
{
// The received message.
ChannelMessage ChannelMessage { get; }

// Instance of BusAccess appropriate for the lifetime of the channel message.
IBusAccess BusAccess { get; }
}

**Note:** In versions of the API less than 2.5, the `IChannelSubscriber` interface was used to subscribe to channel messages (rather than `IChannelMessageSubscriber`).

A ChannelSubscriber can be subscribed to a channel using SubscribeToChannel() in IBusAccess. Note that a single subscriber may subscribe to multiple channels at once. There is enough data in each of the subscriber methods for the subscriber to identify the channel to which the message pertains.

Shared Channels

Clients can also subscribe to a shared channel by shared name, channel name and bindings. In this case, when multiple instances are subscribed to the same channel with the same shared name, only one instance will receive each message.

1
2
3
4
5
bus.SubscribeToChannel(channelName, mySubscriber, new ChannelOptions("shared.name"), new AnonAsyncOpResult() {
onError = (OperationError error) => {
Console.WriteLine("Failed to subscribe to channel: " + channelName + ", error: " + error.Message + ".");
}
});

Using Bindings

Bindings can be used to filter which messages are sent to subscribers. When subscribing to a channel, pass along a list of bindings to specify which messages are relevant to the subscriber:

1
2
3
4
5
6
7
8
9
var bindings = new List<String>();
bindings.Add("log.CalculatorService.*");
bindings.Add("log.*.info");
bindings.Add("log.*.warning");
bus.SubscribeToChannel(channelName, bindings, service, new AnonAsyncOpResult() {
onError = (OperationError error) => {
Console.WriteLine("Failed to subscribe to channel: " + channelName + ", error: " + error.Message + ".");
}
});

The binding string will filter messages based on their dot-separated subject. The '*' character can be used as a wildcard to match entire words. For example, "com.*.info" matches "com.MapService.info" but "com.Map*.info" does not.

Unsubscribing from Bindings

To unsubscribe your subscriber from specific bindings:

1
2
3
4
5
6
7
8
var bindings = new List<String>();
bindings.Add("log.*.info");
bindings.Add("log.*.warning");
bus.UnsubscribeFromBindings(loggingChannelName, bindings, mySubscriber, new AnonAsyncOpResult() {
onError = (OperationError error) => {
Console.WriteLine("Failed to unsubscribe from channel: " + loggingChannelName + ", error: " + error.Message + ".");
}
});

Unsubscribing from a Channel

To unsubscribe your subscriber from specific channel:

1
2
3
4
5
bus.UnsubscribeFromChannel(loggingChannelName, mySubscriber, new AnonAsyncOpResult() {
onError = (OperationError error) => {
Console.WriteLine("Failed to unsubscribe from channel: " + loggingChannelName + ", error: " + error.Message + ".");
}
});

To unsubscribe your subscriber from all channels:

1
2
3
4
5
bus.UnsubscribeFromChannels(mySubscriber, new AnonAsyncOpResult() {
onError = (OperationError error) => {
Console.WriteLine("Failed to unsubscribe from all channels", error: " + error.Message + ".");
}
});

Remote Zone and Multi-Zone Channel Communications

The default behavior of the Avid Connector API is to scope channel communications within the local zone. All the examples given above use this default behavior. If the local zone has been initialized in a multi-zone environment, however, it is possible to communicate using channels across multiple zones.

Interacting with Channel Subscribers in a Specific Remote Zone

To communicate with channel subscribers in a specific remote zone, the bus.Zone(String zoneID) object should be used. The following examples send channel events and messages to subscribers in a specific zone only:

1
2
3
4
5
bus.Zone("5b2123f1-3f8e-4fcb-9263-f7b98bbdab0c").PostToChannel(channelMessage, new AnonAsyncOpResult() {
onError = (OperationError error) => {
Console.WriteLine("Failed to post to channel: " + channelMessage.ChannelId + ", error: " + error.Message + ".");
}
});

In the above case, only multi-zone subscribers to the channel in the remote zone with ID 5b2123f1-3f8e-4fcb-9263-f7b98bbdab0c receive these channel events and messages. If there are local scope subscribers listening on the same channel in that zone, they do not receive the messages (since they only receive messages sourced from their local zone).

Interacting with Channel Subscribers in All Zones

To communicate with channel subscribers in all connected zones, use the bus.MultiZone object.

1
2
3
4
5
bus.MultiZone.PostToChannel(channelMessage, new AnonAsyncOpResult() {
onError = (OperationError error) => {
Console.WriteLine("Failed to post to channel: " + channelMessage.ChannelId + ", error: " + error.Message + ".");
}
});

In the above example, all multi-zone subscribers to the channel across all connected zones receive the channel events and messages. The local scope subscribers in the poster's zone also receive the messages. The local scope subscribers in remote zones, however, do not receive the messages.

Subscribing to Multi-Zone Channels

If the local zone has been initialized in a multi-zone environment, channel subscribers can listen using the multi-zone scope. This means that they can receive channel messages from posters in remote zones.

To subscribe to a channel in the multi-zone scope, use the bus.MultiZone object:

1
2
3
4
5
bus.MultiZone.SubscribeToChannel(channelName, mySubscriber, new AnonAsyncOpResult() {
onError = (OperationError error) => {
Console.WriteLine("Failed to subscribe to channel: " + channelName + ", error: " + error.Message + ".");
}
});

Local Zone Communications

Note that there are also channel methods in the bus.LocalZone object. Invoking channel methods on this object is functionally equivalent to invoking the same methods on the base bus object.

Accessing Constants

The Avid Connector API provides constants, which represent core Avid Platform service types (e.g. registry, federation and attributes). To access these constants:

1
using Avid.Platform.Bus.Messaging.CoreServices;

Exposing Operations as REST Requests

To declare that an operation supports a REST request, apply the RestRequest attribute to corresponding operation:

1
2
3
4
5
6
7
[Operation("subtract", "Returns the difference of two numbers.")]
[RestRequest("calculator/subtract")]
[return: Result("difference")]
public void Subtract(int a, int b, IOperationContext<int> responder)
{
responder.Respond( a - b);
}

The RestRequest annotation has the following parameters:

  • path - Required;
  • method - Not required, default GET;
  • queryParams - Not required, default *;
  • bodyParam - Not required, default *.

For more detailed information about how REST requests are mapped and delivered to your service, please view the upstream HTTP docs.

Migrating to the Asynchronous Methods of IBusClient

In version 2.3 of the .NET Connector API, asynchronous versions of the Query, Send, Broadcast, and PostToChannel methods were added to the IBusClient interface.

Overloaded versions of the Query method were added that accept an additional IAsyncQueryResult parameter. A method of IAsyncQueryResult will eventually get called whenever the Query method completes:

1
2
3
4
5
6
7
8
9
public interface IAsyncQueryResult : IAsyncOpResult {
void OnTimeout();
}

public interface IAsyncOpResult
{
void OnSuccess(object result);
void OnError(OperationError error);
}

Overloaded versions of the Send, Broadcast, and PostToChannel methods each accept an additional IAsyncOpResult parameter.

Migrating to these asynchronous methods is easly done by implementing the desired delegates of the AnonAsyncOpResult, AnonAsyncOpResult<T> or AnonAsyncQueryResult classes.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class AnonAsyncOpResult : IAsyncOpResult {
public Action onSuccess; // This delegate doesn't accept a result parameter i.e. appropriate for "void" IAsyncOpResult operations.
public Action<OperationError> onError;

// ...
}

public class AnonAsyncOpResult<T> : IAsyncOpResult {
public Action<T> onSuccess;
public Action<OperationError> onError;

// ...
}

public class AnonAsyncQueryResult : IAsyncQueryResult {
public Action<Message> onSuccess;
public Action<OperationError> onError;
public Action onTimeout;

// ...
}

For example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ...
var msg = new Message("avid.acs.calculator", "global.test", 3, "divide");
msg.Parameters.Add("num1", 12);
msg.Parameters.Add("num2", 3);
bus.Query(msg, new AnonAsyncQueryResult() {
onSuccess = (Message result) => {
int q = result.Results.GetItem & lt; int&gt; ("quotient");
Console.WriteLine("quotient = " + q + ".");
},
onError = (OperationError error) => {
Console.WriteLine("Failed to query service: " + msg.ServiceType + ", error: " + error.Message + ".");
},
onTimeout = () => {
Console.WriteLine("Timed-out querying service: " + msg.ServiceType + ".");
}
});

And:

1
2
3
4
5
6
7
8
9
10
// ...
bus.SubscribeToChannel("net.service.jobs", new string[] { "submit" }, new SubmitSubscriber(),
new AnonAsyncOpResult() {
onSuccess = () => {
Console.WriteLine("Subscription to 'net.service.jobs':'submit' succeeded.");
},
onError = (OperationError error) => {
Console.WriteLine("Subscription to 'net.service.jobs':'submit' failed: " + error.Message);
}
});

Migrating to IOperationContext, IChannelContext and IChannelSubscriber interfaces

In version 2.5 of the .NET Connector API, the IResponder and IResponder<T> interfaces have been replaced by the IOperationContext and IOperationContext interfaces (which add the concept of a "context" when processing a service operation). The context is propagated to other services through the IBusAccess instance returned by the IOperationContext.BusAccess property. This instance must be used when making any subsequent calls to the bus during the processing of a service operation.

Migrating code from the IResponder interface to IOperationContext involves a simple "Search and Replace" text edit (IResponder -> IOperationContext) and replacing all calls to any private or global instance of IBusAccess with a call to the IOperationContext.BusAccess property. For example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public void SubmitJob(int jobId, int execTime, IOperationContext<NetServiceResponse> response)
{
if (jobId < 1)
{
IDictionary<string, string> parameters = new Dictionary<string, string>();
parameters.Add("jobId", jobId.ToString());
response.Error(new Error("INVALID_JOB_ID", parameters, "Invalid jobId was provided. Must be integer > 0."));
} else if (execTime < 100)
{
IDictionary<string, string> parameters = new Dictionary<string, string>();
parameters.Add("execTime", execTime.ToString());
response.Error(new Error("INVALID_JOB_ID", parameters, "Invalid execution time provided. Must be integer >= 100."));
} else
{
PostChannelMessages(response.BusAccess, jobId, execTime);
SetNumberOfSubmittedJobsCallback setNumberOfSubmittedJobsCallback = new SetNumberOfSubmittedJobsCallback(response);
GetNumberOfSubmittedJobsCallback getNumberOfSubmittedJobsCallback = new GetNumberOfSubmittedJobsCallback(this, setNumberOfSubmittedJobsCallback, response);
GetNumberOfSubmittedJobs(response.BusAccess, getNumberOfSubmittedJobsCallback);
}
}

private void PostChannelMessages(IBusAccess busAccess, int jobId, int execTime)
{
ChannelMessage message = new ChannelMessage("net.service.jobs", "submit");
message.Data.Add("jobId", jobId);

try
{
busAccess.PostToChannel(message, new AnonAsyncOpResult() {
onError = (OperationError error) => {
Console.WriteLine("Post to channel 'net.service.jobs':'submit' failed: " + error.Message);
}
});
}
catch (BusAccessException e)
{
Console.WriteLine(e.Message);
}

System.Timers.Timer timer = new System.Timers.Timer();
timer.Elapsed += (sender, e) => { MessageProcessed(busAccess, jobId); timer.Close(); };
timer.Interval = execTime;
timer.Enabled = true;
}

The IChannelSubscriber interface has also been replaced with a context aware IChannelMessageSubscriber. The IChannelMessageSubscriber.OnChannelMessage method is passed an instance of IChannelContext, which has properties to access the incoming ChannelMessage and the context aware instance of IBusAccess. This instance must be used when making any subsequent calls to the bus during the processing of the channel message.