Dependencies & isolation
Dependencies & isolation
Which assemblies the host already provides, when a third-party library belongs next to your plugin and when it must not, and the load-context model that makes "DLL only" the rule for contracts but not for your own dependencies.
On this page
What the host provides vs what you ship
The TradeStrike.Sdk package is a meta-package: it bundles the contract assemblies you compile
against. The host already loads those assemblies, so you reference them at build time but
never deploy them. The split is simple:
| Assembly | Provided by the host? | Deploy with your plugin? |
|---|---|---|
TradeStrike.Pipeline.Contracts |
Yes | No |
TradeStrike.Pipeline.Trading.Contracts |
Yes | No |
TradeStrike.Pipeline.Strategies.Contracts |
Yes | No |
| The .NET runtime / framework assemblies | Yes | No |
| A NuGet library the host does not provide (CSV parser, exchange SDK, …) | No | Yes |
| Your own plugin DLL | — | Yes |
TradeStrike.Pipeline* → never deploy (the host
owns it). Your own private third-party dependencies → deploy alongside the plugin DLL.
Third-party dependencies
The "deploy your DLL only" rule applies to framework and contract assemblies the host already provides. It does not apply to genuine third-party libraries the host knows nothing about.
If your plugin references a NuGet library the host does not provide — a CSV parser, a JSON
serializer it doesn't ship, an exchange SDK — that library does belong next to
your plugin DLL in the Plugins folder. The plugin's isolated load context resolves such
"sidecar" assemblies from its own folder through an AssemblyDependencyResolver built over the
plugin's path. Put the sidecar DLL in the same folder and it just works.
DeployToAlgoStudioPlugins target copies $(TargetPath) (your DLL) and its PDB
— not your dependency tree. If your plugin has a genuine sidecar dependency, copy that
one DLL into the Plugins folder yourself, or add your own copy step. This is deliberate:
it stops the target from duplicating host-provided assemblies into the plugin folder, which would break
type identity.
The isolation model
Each plugin DLL is loaded into its own collectible AssemblyLoadContext — named
Plugin:<filename>. Isolation is deliberate: it lets two plugins reference
different versions of the same private library without colliding, and it keeps one plugin's
dependency tree from leaking into another's. Here is what the installer runs at startup:
graph TD
A[Host startup] --> B[PluginInstaller.Install]
B --> C[PluginLoader.ScanDirectory]
C --> D[For each DLL: new PluginLoadContext]
D --> E[Load DLL into its own ALC]
E --> F[Catalog public types by contract]
F --> G[Register indicators / columns; strategies and drawing tools picked up from the loaded assembly]
G --> H[Write summary + errors to the Control Center Log tab]
- At startup,
PluginInstaller.InstallcallsPluginLoader.ScanDirectoryon thePluginsfolder. - Each
*.dllis loaded into its own collectibleAssemblyLoadContextnamedPlugin:<filename>. - The loader catalogs the public, zero-arg-constructable types by the contract each implements; indicators (and Market Analyzer columns) are registered into the shared catalogs, and the now-loaded assembly makes its strategies and drawing tools discoverable.
- A one-line summary — plus any load or registration errors — is written to the Control Center Log tab.
A missing folder, a malformed DLL, or a single failing type never aborts startup — the failure is captured in the report and the rest of the app continues.
Why contracts must come from the host
Here is the crux of why isolation forces the deployment rule. The host and your plugin must agree on a
shared vocabulary of types — IIndicator, IStrategy, and the rest of the
contracts. In .NET, type identity is not just the type's name: it is the name plus the exact
assembly instance it was loaded from. If a contract assembly were resolved inside the plugin's
isolated context from a copy you shipped, the runtime would hold two distinct
IIndicator types — the host's and yours — that look identical but are not
interchangeable.
The plugin load context prevents this by refusing to load the contract assembly into the plugin
context: when the runtime asks the plugin context for the assembly that defines IIndicator,
the context returns nothing and lets resolution fall through to the host's default context. Every other
request is resolved against the plugin's own folder — which is exactly why a sidecar dependency in
that folder is found, and why a stray copy of a contract DLL would be found instead of the
host's, breaking identity.
IIndicator is always resolved from the host's context, so
typeof(IIndicator) is literally the same type object on both sides. Ship your own copy and
the host's assignability check sees your indicator implement a different IIndicator,
fails, and skips the plugin. Your private third-party deps have no such shared-identity requirement
— which is exactly why they are safe, and expected, to live in the Plugins folder
where the isolated context resolves them.
Version conflicts & targeting
Because each plugin gets its own load context, two plugins can ship different versions of the same third-party library and neither poisons the other. There is one constraint you must respect, though: your plugin must target a framework compatible with the host.
-
Target framework. The contract assemblies target
net10.0, so your plugin must targetnet10.0(or higher within the same major) — the same baseline as the TradeStrike host. A plugin built for an older framework will fail to load. -
Don't shadow host assemblies. Never ship a different version of a
TradeStrike.Pipeline*contract assembly to "pin" a version. You cannot win that fight — the contract is always resolved from the host — and shipping a copy only risks the identity break above. - Private deps are yours to version. For genuine third-party libraries, ship whatever version you compiled against; isolation keeps it from clashing with other plugins or the host.