16.1 introduces the Object Registry
service. This service is designed to let you register an object to be made available anywhere within the same process. The initial use-case is to allow the registration of a device configuration, and subsequent reading of that configuration by other devices.
This service is available in both Connexion and Remote Agent platforms.
This feature required us to add deterministic channel load order to Connexion and Remote Agent. In order to ensure that objects are registered before they are read, we have created the concept of a Non Processing Channel
. A Non Processing Channel
is loaded prior to regular channels. Read about Non Processing Channels.
While sharing a device configuration object may seem trivial (via static objects), every loaded device which consumes this configuration must be compiled against (and loaded from) the same assembly. Upgrading the plugin which registers a configuration would cause all consuming devices to break (and require them all to be upgraded).
The Object Registry
service removes this requirement by providing each consuming device with a copy of the configuration in the version they are expecting. This allows the upgrades of individual plugins without breaking any other consumers**.
** Internally we are using a mechanism similar to serialization. Any properties you wish to share must be marked with the [DataMember]
attribute. Additionally, if you make a change that would break deserialization (such as renaming a property or changing its type), this may also be a breaking change in the registry service.
Unlike static objects, the Object Registry
allows objects to be registered by key and type. This allows you to scope your shared objects (for example, by group, tab, channel, or globally).
The Object Registry service is exposed by the ServiceProvider
interface as well as the MessageChannel
property exposed to each device. An object can be registered using the Register(object key, object toRegister)
method.
public override void Load(string configuration) { base.Load(configuration); MessageChannel.ObjectRegistryService.Register(MessageChannel.GroupKey, Configuration); }
In the above example, a device configuration is being registered at the group level (by using the GroupKey
property). This object can be read by other devices by using the Get<T>(object key)
method:
var configuration = MessageChannel.ObjectRegistryService.Get<SharedObjectType>(MessageChannel.GroupKey)
The typical architecture for sharing device configuration is to place the configuration class into a separate shared assembly. The configuration publisher device and any consumer devices will reference this shared assembly. A sample device configuration might look as follows:
using Connexion.Core; using System.Collections.Generic; using System.Runtime.Serialization; namespace Shared { [DataContract] public class SharedConfiguration : NotifyBase { private string m_PropertyA; [DataMember] public string PropertyA { get { return m_PropertyA; } set { if (m_PropertyA != value) { m_PropertyA = value; RaisePropertyChanged(); } } } private SafeTrackerEx<MapTable> m_MapTables; [DataMember] public IList<MapTable> MapTables { get { if (m_MapTables == null) { m_MapTables = new SafeTrackerEx<MapTable>(); m_MapTables.CollectionChanged += (o, e) => RaisePropertyChanged(); m_MapTables.MemberPropertyChanged += (o, e) => RaisePropertyChanged(); } return m_MapTables; } } } }
In order to share this object, it must be registered by the device which configures it. To do this, we can use the following. In this example, we are use the GroupKey as the object key. This effectively scopes this shared object to all channels within this group.
using Connexion.Core; using Shared; using System; namespace ConfigOwnerDevice { [DevicePlugin("Configuration Master Device", "Hosts a shared configuration", DeviceDefinitionFlags.NonProcessingDevice, typeof(object), typeof(object), typeof(SingletonTestingFactory))] public class ConfigOwnerDevice: BaseDevice<SharedConfiguration> { public ConfigOwnerDevice(Guid deviceKey, IMessageChannelDevice messageChannelDevice) : base(deviceKey, messageChannelDevice) { } public override void Load(string configuration) { base.Load(configuration); // register our configuration for other devices to read MessageChannel.ObjectRegistryService.Register(MessageChannel.GroupKey, Configuration); } } }
A device which wants to read this configuration could be coded as follows:
using Connexion.Core; using Shared; using System; using System.Threading; using System.Threading.Tasks; namespace ConfigurationConsumer { [DevicePlugin("Configuration Consumer", "Consume configuration of a different device", DeviceDefinitionFlags.None, typeof(object), typeof(object), typeof(ConsumerFactory))] public class ConfigurationConsumer : BaseDevice<ConsumerConfiguration>, IConfigurationConsumer { private SharedConfiguration m_Configuration; // the shared config private int m_SomeCachedVal; // sample cached property private int m_ConfigurationVersion; // used to display the shared configuration in this device's UI public ConfigurationConsumer(Guid deviceKey, IMessageChannelDevice messageChannelDevice) : base(deviceKey, messageChannelDevice) { // get a handle to the shared configuration UpdateConfiguration(); // get notified when the configuration is re-published MessageChannel.ObjectRegistryService?.RegisterForEvent<SharedConfiguration>(MessageChannel.GroupKey, ObjectRegistryService_OnRegistrationUpdated); } private void ObjectRegistryService_OnRegistrationUpdated() { // fired when the configuration object is replaced UpdateConfiguration(); } private void UpdateConfiguration() { // get a handle to the configuration for our group m_Configuration = MessageChannel.ObjectRegistryService?.Get<SharedConfiguration>(MessageChannel.GroupKey); if (m_Configuration == null) return; // if we have cached values, we should update them to the new value from the shared configuration m_SomeCachedVal = m_Configuration.ConfigurationB; m_ConfigurationVersion++; } public override async Task ProcessMessageAsync(IMessageContext context, CancellationToken token) { // your logic here } // this method is called by the UI to display the current configuration values public Task<GetConfigurationResponse> GetConfigurationAsync(GetConfigurationRequest request) { return Task.FromResult(new GetConfigurationResponse(m_ConfigurationVersion, m_ConfigurationVersion == request.Version ? null : m_Configuration)); } } }
Download the sample device solution here.
Download the sample channels here.
If you import the sample channels into a new tab, you should have a Config Source
channel and a Config Consumer
channel. You’ll notice the Config Source
channel has a different title color and a hatched background. This is a Non-Processing
channel which is loaded before regular channels.
If you edit this channel, you’ll notice the Non Processing Channel
checkbox is set.
The Shared Config Source
device has a simple UI to edit a few fields as well as a map tables tab. This UI edits the configuration that is shared with other devices. The Configuration Consumer
device (in the Config Consumer channel) uses code similar to the above examples (device code) to get a reference to the shared configuration. The device UI displays a json-serialized version of the configuration so we can easily visualize the consumers view of the shared configuration. This UI refreshes every 5 seconds (while the UI is visible), so we can make changes to the source configuration and then visualize that change in the consumer device.
Make a change to the source device UI and save the channel:
And then switch back to the consumer:
In practice, your consumer devices will probably have their own configuration and UI, and simply reference shared fields at runtime.