Worker SDK in C#

Step 2. Address major API breaking changes - Worker SDK in C#

  • See the Upgrade guide page for step 1, before following this page.

1. Update packages

Note: If your project is using generated build scripts, then you can skip this section and go to 2. Update component metaclasses.

The package structure of the Worker SDK in C# has been cleaned up slightly in SpatialOS 14, so you will need to make some adjustments within your build system:

a. We've renamed the C# assembly contained within the csharp worker package from Improbable.WorkerSdkCsharp.dll to
Improbable.Worker.dll.
b. We've removed the "Core SDK". Instead, you should obtain dynamic builds of the Worker SDK in C. Worker SDK in C packages are listed here. As a result, the native library which is loaded at runtime is now called improbable_worker instead of CoreSdkDll, so you need to make changes in the .csproj to ensure that the right native library is copied to the bin folder. In summary, make the following changes:

a. Replace core-dynamic-x86_64-win32 with c-dynamic-x86_64-vc140_mt-win32.
b. Replace core-dynamic-x86_64-macos with c-dynamic-x86_64-clang-macos.
c. Replace core-dynamic-x86_64-linux with c-dynamic-x86_64-gcc510-linux.
d. Replace any reference to CoreSdkDll.dll with improbable_worker.dll, libCoreSdkDll.dylib with
libimprobable_worker.dylib, and libCoreSdkDll.so with libimprobable_worker.so.

2. Update component metaclasses

One recurring pain point when using the Worker SDK in C# was the usage of many intermediate wrapper types when sending and receiving data. This would involve ugly constructs, such as having to wrap your component data type in an outer MyComponent.Data type, or access the concrete data in a dispatcher callback by calling .Value repeatedly.
To solve this, we have reworked the use of generics throughout the Worker SDK in C#. Previously, in order to determine which component or command was being manipulated, some functions in the Connection took a component or command metaclass generic parameter. Instead, these functions now have an overload taking an IComponentMetaclass or ICommandMetaclass instance parameter to determine the component type instead, then a number of generic parameters are automatically deduced from this. The methods which depend on metaclasses are:

  • Connection.SendAuthorityLossImminentAcknowledgement
  • Connection.SendComponentUpdate
  • Connection.SendAddComponent
  • Connection.SendRemoveComponent
  • Connection.SendCommandRequest
  • Connection.SendCommandResponse
  • Connection.SendCommandFailure
  • Dispatcher.OnAddComponent
  • Dispatcher.OnRemoveComponent
  • Dispatcher.OnAuthorityChange
  • Dispatcher.OnComponentUpdate
  • Dispatcher.OnCommandRequest
  • Dispatcher.OnCommandResponse

However, to ease migration, the previous functions without a metaclass instance parameter have been left behind and deprecated. Therefore, when upgrading to SpatialOS 14, you can either keep the old syntax or move to the new syntax. To keep the old syntax, follow the steps in section 2.1 and skip section 2.2. To migrate to the new syntax, skip section 2.1 and follow 2.2.

2.1 Keep using the old syntax

Connection methods that take a component or command metaclass as a generic parameter, such as SendComponentUpdate, are unchanged (but are now deprecated). However, callback methods in the Dispatcher have changed slightly to accomodate the new syntax. As a result, you need to make the following changes in the signatures of your dispatcher callbacks:

a. Change AddComponentOp<T> to AddComponentOpBoxed<T>.
b. Change ComponentUpdateOp<T> to ComponentUpdateOpBoxed<T>.
c. Change CommandRequestOp<T> to CommandRequestOpBoxed<T>.
d. Change CommandResponseOp<T> to CommandResponseOpBoxed<T>.

You may also need to suppress the CS0618 "Obsolete" warning by modifying your project settings, or surrounding your code in a block such as:

#pragma warning disable 618
// code using the old syntax.
#pragma warning enable 618

Once this is done, you can skip section 2.2 and move straight to section 3 in this guide.

2.2 Migrate to the new syntax

Connection and Dispatcher methods that take a component or command metaclass as a generic parameter, such as SendComponentUpdate or OnComponentUpdate, now require a static IComponentMetaclass or ICommandMetaclass instance to be specified as the first parameter. This instance will be a static property called Metaclass on any generated component or command metaclass, for example, given MyComponent, the metaclass object will be called MyComponent.Metaclass.

In addition, intermediate types that implement IComponentData, ICommandRequest or ICommandResponse (such as MyComponent.Data or MyCommand.Request), are no longer used in either Connection methods or Dispatcher callbacks. Therefore, when passing data to or from any of these methods, you need to remove any usage of these intermediate types. You can find some examples of changes below.

This is how sending a component update has changed (assuming MyComponent is a component defined in schema):

cs
// before
var update = new MyComponent.Update();
update.SetInt(30);
connection.SendComponentUpdate<MyComponent>(entityId, update, ...);
// after
var update = new MyComponent.Update();
update.SetInt(30);
connection.SendComponentUpdate(MyComponent.Metaclass, entityId, update, ...);

This is how sending a command request has changed (assuming TestCommand has a request type called MyRequest in schema):

// before
var request = new MyRequest(123, 89.0);
connection.SendCommandRequest<MyComponent.Commands.TestCommand>(entityId, new MyComponent.Commands.TestCommand.Request(request), ...);
// after
var request = new MyRequest(123, 89.0);
connection.SendCommandRequest(MyComponent.Commands.TestCommand.Metaclass, entityId, request, ...);

This is how receiving an AddComponentOp has changed:

cs
// before
dispatcher.OnAddComponent<MyComponent>(op => {
    MyComponentData data = op.Data.Get().Value;
    // use data...
});
// after
dispatcher.OnAddComponent(MyComponent.Metaclass, op => {
    MyComponentData data = op.Data;
    // use data...
});

3. Update usage of the snapshot API

The way snapshot errors are reported has now changed, to allow distinction between different types of errors. In C# the different snapshot errors are reported by the exceptions StreamBadStateException, StreamInvalidDataException, and System.IO.EndOfStreamException being thrown. The signature of the snapshot API methods has changed as well to reflect this.

3.1 Saving snapshots

All SnapshotOutputStream methods may fail. Previously Option<string> was used to signal errors, for example like this:

Dictionary<EntityId, Entity> entities = myEntities();
SnapshotOutputStream outputStream = new SnapshotOutputStream(newSnapshotFilename);

foreach (KeyValuePair<EntityId,Entity> entry in entities)
{
    var error = outputStream.WriteEntity(entry.Key, entry.Value);
    if (error.HasValue)
    {
        // Log error.
        break;
    }
}
// Write EOF and release the stream's resources.
outputStream.Dispose();

To upgrade, rewrite your code to use a try-catch block around your snapshot processing to catch the exceptions and distinguish between different kinds of errors:

Dictionary<EntityId, Entity> entities = myEntities();
SnapshotOutputStream outputStream = new SnapshotOutputStream(newSnapshotFilename);
 
foreach (KeyValuePair<EntityId,Entity> entry in entities)
{
    var error = outputStream.WriteEntity(entry.Key, entry.Value);
    if (error.HasValue)
    {
        // Log error.
        break;
    }
}
// Write EOF and release the stream's resources.
outputStream.Dispose();

3.2. Loading snapshots

All SnapshotInputStream methods may fail. Previously Option<string> was used to signal errors, for example like this:

cs
var entities = new Dictionary<EntityId, Entity>();
SnapshotInputStream inputStream = new SnapshotInputStream(snapshotFilename);

EntityId entityId;
Entity entity;

while (inputStream.HasNext())
{
    var error = inputStream.ReadEntity(out entityId, out entity);
    if (error.HasValue)
    {
        // Log error.
        break;
    }
    entities.Add(entityId, entity);
}
// Release the stream's resources.
inputStream.Dispose();

To upgrade, use a try-catch block around your snapshot processing to catch the exceptions and distinguish between different kinds of errors. ReadEntity now takes no parameters and returns a System.Collections.Generic.KeyValuePair<EntityId, Entity> object:

var entities = new Map<EntityId, Entity>();
try
{
    SnapshotInputStream inputStream = new SnapshotInputStream(filePath);
 
    while (inputStream.HasNext()) // This check can be done alternatively by catching EndOfStreamException.
    {
        try
        {
            var snapshotEntity = inputStream.ReadEntity();
            entities.Add(snapshotEntity.Key, snapshotEntity.Value);
        }
        catch (StreamInvalidDataException ex)
        {
            // Handle or log operation failure.
            // The last read operation failed, but the snapshot is still usable.
        }
        catch (StreamBadStateException ex)
        {
            // Log error.
            // An internal error occurred when reading and the snapshot is not usable.
            break;
        }
        catch (System.IO.EndOfStreamException ex)
        {
            // The eof was reached when reading. Not an error if used as alternative to HasNext.
            break;
        }
    }
    // Release the stream's resources.
    inputStream.Dispose();
}
catch (StreamBadStateException ex)
{
    // Log error.
    // The snapshot failed to be initialized, and the stream is not usable.
}

Updated about a year ago


Next

Now return to the Upgrade guide and follow step 3.

Upgrade guide

Worker SDK in C#


Suggested Edits are limited on API Reference Pages

You can only suggest edits to Markdown body content, but not to the API spec.