Esatto.AppCoordination.Common
4.0.0
See the version list below for details.
dotnet add package Esatto.AppCoordination.Common --version 4.0.0
NuGet\Install-Package Esatto.AppCoordination.Common -Version 4.0.0
<PackageReference Include="Esatto.AppCoordination.Common" Version="4.0.0" />
paket add Esatto.AppCoordination.Common --version 4.0.0
#r "nuget: Esatto.AppCoordination.Common, 4.0.0"
// Install Esatto.AppCoordination.Common as a Cake Addin #addin nuget:?package=Esatto.AppCoordination.Common&version=4.0.0 // Install Esatto.AppCoordination.Common as a Cake Tool #tool nuget:?package=Esatto.AppCoordination.Common&version=4.0.0
Esatto Application Coordination
Allow multiple applications to communicate with each other within the same session. A "shell" application exposes the fact that an entity is open, and other applications can add commands or otherwise change their behavior based on the shell application.
A background process called the coordinator receives notices from each coordinated application and distributes them to all connected clients.
Supports communication across multiple machines using Remote Desktop Virtual Channels to allow applications virtualized with RemoteApp to communicate with each other and with local applications.
How it works
Each application publishes "entries" in the database. Each entry has an address consisting of the
identity of the publishing application (Path
) and the semantic Key
. The Key
remains constant,
but is not unique (without Path
). Path
and Key
are both /
separated lists of nodes. Nodes may be any string that does not contain
the characters /
or :
.
The coordinator is responsible for maintaining a single view of the database across all applications. The set of all published entries is distributed back to each coordinated application asynchronously.
An example database as viewed by "Application 1" is where a document editor is open on two systems and "Application 2" is exposing commands would be:
{
"Entries": {
"/App1/1/:/Entity/Document1/": { "DisplayName": "Document 1", "DocumentId": 1 },
"/VDI1/App2/1/:/Entity/Document1/Command/": { "DisplayName": "Fax" },
"/VDI1/App2/2/:/Entity/Document1/Command/": { "DisplayName": "Email" },
"/VDI1/App2/1/:/Entity/Document2/Command/": { "DisplayName": "Fax" },
"/VDI1/App2/2/:/Entity/Document2/Command/": { "DisplayName": "Email" },
"/RemoteApp/App4/1/:/Entity/Document2/": { "DisplayName": "Document 2", "DocumentId": 2 },
}
}
The coordinators will change the path as an entry flows through the system. The path will be prefixed with the source from which the entry was learned. As an example, the entry "/Entity/Document1/Command/" was published on "VDI1" by "App2" and viewed from the console by "App1". As it flowed, the path changed from:
- "/1/" as originally published by App2 on VDI1
- "/App2/1/" as published by the coordinator on VDI1
- "/VDI1/App2/1/" as published by the coordinator on the console
The inverse would happen for "/Entity/Document1/" published on the console and viewed on VDI1:
- "/1/" as originally published by App1 on the console
- "/App1/1/" as published by the coordinator on the console
- "/Console/App1/1/" as published by the coordinator on VDI1
Communication:
IPC
Any entry can be "Invoked" by passing a request payload and receiving a response payload. Failures are propagated back to the caller as exceptions. The format of the payload is not defined. Operations may not take more than 10 seconds to complete. Payload may not exceed 1 MB. If longer operations are required, register a "completion" entry and asynchronously "return" the result as a new invoke.
Usage
To build an application which uses Esatto.AppCoordination
, add a reference to Esatto.AppCoordination.Common
:
<ItemGroup>
<PackageReference Include="Esatto.AppCoordination.Common" Version="4.0.0-*" />
</ItemGroup>
Create a connection to the coordinator using CoordinatedApp.Connect
then publish entries:
// Create a connection which will keep all events and callbacks on the current sync ctx
// and will not throw exceptions if no coordinator is available
using var app = new CoordinatedApp(SynchronizationContext.Current, silentlyFail: false, logger);
// Publish an entry to all applications with key /Entity/Example 1234/
var entry1 = ThisApp.Publish(CPath.From("Entity", $"Example {Process.GetCurrentProcess().Id}"), new()
{
{ "DisplayName", "Example 1" },
{ "example", "value" },
{ "poke", 1 },
{ "entityNumber", entityNumber }
})
// later, update the value of the entry
var updatedValue = entry1.Value.Clone();
updatedValue["poke"] = 2;
entry1.Value = updatedValue;
// or replace it
entry1.Value = new EntryValue() { { "poke", 3 } };
// retrieve the raw JSON
var jsonData = entry1.Value.JsonValue;
// replace with explicit JSON
entry1.Value = new EntryValue("{ \"poke\": 4 }");
// un-publish the entry by disposing it
entry1.Dispose();
// Publish an invokable entry as "/Example/Command1/"
using var entry2 = app.Publish(CPath.From("Example", "Command1"), new(), payload =>
{
Console.WriteLine($"Received payload: {payload}");
return "Ok";
});
Entries from other applications can be accessed via CoordinatedApp.ForeignEntities
:
foreach (var entry in app.ForeignEntities)
{
logger.LogInformation("Entry: {Entry}", entry);
entry.ValueChanged += (sender, e) => logger.LogInformation("Entry {Entry} changed", entry);
entry.Removed += (sender, e) => logger.LogInformation("Entry {Entry} removed", entry);
}
Entries matching a particular key can be monitored via CoordinatedApp.ForeignEntities.CreateProjection(string)
:
// watch for all entries with the key "/Handler/DocumentPrinted/"
using var obs1 = app.ForeignEntities.CreateProjection("/Handler/DocumentPrinted/");
obs1.CollectionChanged += (sender, e) => logger.LogInformation("Collection changed: {Change}", e);
// watch for all entries with the prefix "/Entity/"
using var obs2 = app.ForeignEntities.CreateProjection(key => key.StartsWith(CPath.From("Entity"));
obs2.CollectionChanged += (sender, e) => logger.LogInformation("Collection changed: {Change}", e);
Entries can be invoked via ForeignEntry.Invoke
:
var entry = app.ForeignEntities.FirstOrDefault(k => k.Key == "/Example/Command1/")
?? throw new InvalidOperationException("Could not find Command1");
var result = entry1.Invoke("Hello World");
Tooling
A demo client is avaialble to view the database. It is installed by default on each coordinated system.
It can be launched from C:\Program Files\Esatto\AppCoord2\Esatto.AppCoordination.DemoClient.exe
.
Redistribution
An application may redistribute Esatto.AppCoordination.Common
, and may opportunistically use AppCoord
by passing silentlyFail: true
to the Connect
method. AppCoord is installed via the MSI installer.
A setup bootstrapper may check for an existing installation by detecting the presence of the registry
key HKEY_CLASSES_ROOT\CLSID\{13853D88-306E-452E-89B1-B655BA3E82D0}
.
Developer setup
In order to develop and test Esatto.AppCoordination
, the following steps are required to
setup the developer machine.
[!NOTE] The following steps are only required for development and testing of AppCoord.
To develop or test an application which depends uponEsatto.AppCoordination
, see the Usage section.
Since interaction between coordinated applications and the coordinator requires COM,
the interfaces and TLB must be registered in HKEY_CLASSES_ROOT
. This registration
may be done by running regasm
from an elevated command prompt.
Install:
C:\dev\gitroot\appcoord\Esatto.AppCoordination.Common\bin\Debug\net48>regasm /codebase /tlb Esatto.AppCoordination.Common.dll
Microsoft .NET Framework Assembly Registration Utility version 4.8.9032.0
for Microsoft .NET Framework version 4.8.9032.0
Copyright (C) Microsoft Corporation. All rights reserved.
Assembly exported to 'C:\dev\gitroot\appcoord\Esatto.AppCoordination.Common\bin\Debug\net48\Esatto.AppCoordination.Common.tlb', and the type library was registered successfully
Uninstall:
C:\dev\gitroot\appcoord\Esatto.AppCoordination.Common\bin\Debug\net48>regasm /u /codebase /tlb Esatto.AppCoordination.Common.dll
Microsoft .NET Framework Assembly Registration Utility version 4.8.9032.0
for Microsoft .NET Framework version 4.8.9032.0
Copyright (C) Microsoft Corporation. All rights reserved.
RegAsm : warning RA0000 : No types were un-registered
Type library 'C:\dev\gitroot\appcoord\Esatto.AppCoordination.Common\bin\Debug\net48\Esatto.AppCoordination.Common.tlb' un-registered successfully
In order to auto-start the coordinator, the CLSID's of the coordinator must be registered:
New-Item 'hklm:\SOFTWARE\Classes\CLSID\{13853D88-306E-452E-89B1-B655BA3E82D0}\LocalServer32' -Force `
| Set-ItemProperty -Name '(Default)' -Value 'C:\dev\gitroot\appcoord\Esatto.AppCoordination.Coordinator\bin\Debug\net48\Esatto.AppCoordination.Coordinator.exe'
To have the coordinator registered with MSTSC:
New-Item 'hklm:\SOFTWARE\Classes\CLSID\{281BB6F7-B2A9-40D7-9F02-8856E3EDC505}\LocalServer32' -Force `
| Set-ItemProperty -Name '(Default)' -Value 'C:\dev\gitroot\appcoord\Esatto.AppCoordination.Coordinator\bin\Debug\net48\Esatto.AppCoordination.Coordinator.exe'
New-Item 'hklm:\SOFTWARE\Microsoft\Terminal Server Client\Default\Addins\{281BB6F7-B2A9-40D7-9F02-8856E3EDC505}' -Force `
| Set-ItemProperty -Name 'Name' -Value '{281BB6F7-B2A9-40D7-9F02-8856E3EDC505}'
Product | Versions Compatible and additional computed target framework versions. |
---|---|
.NET | net7.0-windows7.0 is compatible. net8.0-windows was computed. net9.0-windows was computed. |
.NET Framework | net48 is compatible. net481 was computed. |
-
.NETFramework 4.8
- Esatto.Utilities (>= 3.0.3)
- Esatto.Win32.Com (>= 3.0.3)
- Microsoft.Extensions.Logging.Abstractions (>= 7.0.1)
- Newtonsoft.Json (>= 13.0.3)
-
net7.0-windows7.0
- Esatto.Utilities (>= 3.0.3)
- Esatto.Win32.Com (>= 3.0.3)
- Microsoft.Extensions.Logging.Abstractions (>= 7.0.1)
- Newtonsoft.Json (>= 13.0.3)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.