LocalEndpoint / Endpoint Strategy
LocalEndpoints.com Plugin Platform for a Custom C Desktop App
Report summary
For LocalEndpoints.com , the strongest design is a hybrid plugin platform : a manifest-first catalog , a small versioned SDK/contract assembly , in-process plugins loaded with AssemblyLoadContext for trusted low-risk extensions, and out-of-process plugins for anything untrusted, high-privilege, memo
Key topics
- LocalEndpoint / Endpoint Strategy
- LocalEndpoint
- Endpoint Strategy
- .NET
- C#
- SQL
- Runtime
- Rust
- NuGet
Research provenance
For citation, use the report title and canonical URL. Archival presence does not establish authorship or promote report statements into portfolio evidence.
Source availability: 49 citation markers in the source export have no recoverable source links. Those markers are omitted from this reader; any supplied bibliography and ordinary links remain. Check the original sources before relying on the cited claims.
This page renders the archived Markdown as safe, formatted HTML. It is background research and does not become a portfolio claim without evidence review.
Full report
On this page
Executive summary
For LocalEndpoints.com, the strongest design is a hybrid plugin platform: a manifest-first catalog, a small versioned SDK/contract assembly, in-process plugins loaded with AssemblyLoadContext for trusted low-risk extensions, and out-of-process plugins for anything untrusted, high-privilege, memory-unsafe, or dependency-heavy. In current .NET, AssemblyLoadContext is the right primitive for load isolation and cooperative unloading, while AppDomains are not a viable sandbox in .NET 6+ and Code Access Security is deprecated and not honored. That means real trust boundaries must be enforced with process boundaries, AppContainer-style restrictions, or containers, not with in-process CLR features.
If the project must stay within .NET 6/7/8, the practical recommendation is net8.0-windows for the host and either netstandard2.0 or a multi-targeted SDK for plugin contracts, because .NET 6 and 7 are already out of support while .NET 8 remains supported only until November 10, 2026. For a greenfield build starting in mid-2026, Microsoft’s lifecycle data makes a strong case to plan a fast path to .NET 10 LTS, even if an initial release ships on .NET 8 for ecosystem reasons. On UI technology, WinUI 3 is Microsoft’s recommended native framework for new Windows apps, while WPF remains a Windows-only, mature, lower-risk choice for complex shell applications and third-party control ecosystems.
For packaging and delivery, treat the plugin store as a separate platform service under the LocalEndpoints.com brand, not as a thin mirror of NuGet. Use NuGet-compatible packaging where it helps authors and CI/CD, but keep marketplace metadata, compatibility, licensing, trust, staged rollout, telemetry, and entitlement resolution in a LocalEndpoints.com Store API. Sign every binary and installer with Authenticode, sign every plugin package, maintain trusted publisher and repository policies, and use supply-chain controls such as package source mapping, vulnerability auditing, and SBOM generation in CI/CD. Existing plugin ecosystems such as VS Code, Visual Studio VSIX, and JetBrains all rely on explicit manifests, compatibility ranges, dependency declarations, and marketplace metadata; LocalEndpoints.com should do the same, with stricter capability declarations because this is a desktop host, not a text editor.
The recommended business model is to separate the concerns clearly: LocalEndpoints.com operates the marketplace, signing, trust, telemetry, trials, entitlements, and billing orchestration, while plugin authors publish packages and metadata through a publisher portal and CI flow. For payments, the three realistic models are your own PSP stack such as Stripe, a merchant-of-record platform such as Paddle, or Microsoft Store commerce if the host is distributed that way; the merchant-of-record route is operationally attractive if you expect global plugin sales and want tax, refunds, and chargebacks handled centrally. Whichever model you choose, the client should only receive signed entitlements and should validate them against the store periodically, with an offline grace window and server-controlled revocation.
Platform baseline and architecture recommendation
The architectural baseline I recommend for LocalEndpoints.com is:
- Host app:
net8.0-windowsnow, with an explicit migration plan to current LTS. - UI shell: WPF if time-to-market, control maturity, and shell extensibility are the top priorities; WinUI 3 if the product is greenfield and the team wants the most modern Windows-native UX.
- Composition model: Generic Host + DI + logging + configuration in the desktop app, so plugins can receive well-defined services.
- Plugin contract: separate SDK/abstractions package with stable interfaces and DTOs.
- Discovery: manifest-first, not “scan DLLs and hope.”
- Execution: in-process ALC for trusted/verified plugins; out-of-process for risky plugins or plugins that need their own dependency world and failure boundary.
This is the main tradeoff among .NET 6, 7, and 8:
| Target | Support reality | Upside | Downside | Recommendation |
|---|---|---|---|---|
| .NET 6 | Ended support in November 2024. | Large installed base in legacy codebases | Unsupported for new security-sensitive work | Do not start here |
| .NET 7 | Ended support in May 2024. | Short-lived stepping stone only | Unsupported and short-lived by design | Do not start here |
| .NET 8 | LTS through November 10, 2026. | Best choice if constrained to 6/7/8 | Short remaining runway from a 2026 starting point | Best within the requested set |
| .NET 10 | Current LTS in 2026, supported through November 2028. | Best lifecycle runway for a greenfield host | Outside the user’s 6/7/8 comparison scope | Best overall if version is reopened |
For plugin contracts, two patterns are realistic. A netstandard2.0 contract package maximizes compatibility and lets the same abstractions work across multiple .NET implementations; the tradeoff is that authors cannot rely on newer platform APIs in the contract layer. A multi-targeted SDK such as netstandard2.0;net8.0-windows gives better platform ergonomics but slightly raises authoring complexity. Microsoft’s .NET Standard guidance still supports using it for cross-implementation libraries, even though there will be no new .NET Standard versions.
Conceptually, treat the platform as three tiers: host shell, plugin runtime, and store/control plane.
flowchart LR
U[User] --> A[LocalEndpoints.com Desktop App]
A --> S[Shell UI and DI]
S --> R[Plugin Runtime]
R --> I1[In-process trusted plugins]
R --> I2[Out-of-process isolated plugins]
A --> C[Store Client]
C --> API[LocalEndpoints.com Store API]
API --> DB[(Store DB)]
API --> PKG[Package Storage and CDN]
API --> LIC[Licensing and Entitlements]
API --> TEL[Telemetry Pipeline]
API --> PUB[Publisher Portal and CI]
For plugin architecture patterns, the default should be contract-first + manifest-first + ALC. MEF remains useful for composition, but it is not your primary loading/isolation boundary.
| Pattern | What it gives you | What it does not give you | Fit for LocalEndpoints.com |
|---|---|---|---|
Contract assembly + manifest + AssemblyLoadContext | Explicit contracts, dependency isolation, possible unloading, side-by-side dependency versions. | Not a security boundary; unload is cooperative, not forced. | Best default |
| MEF | Lightweight discovery/composition with low configuration. | Not a modern security or dependency isolation model by itself | Good for optional composition inside the plugin host |
| Reflection-only DLL scanning | Simple to prototype | Fragile, eager loading risks, weak marketplace metadata model | Avoid as the main publishing/discovery design |
| Separate process plugin host | Crash isolation, true permission boundary, easier native dependency separation | IPC complexity, more UX and deployment work | Best for untrusted or privileged plugins |
A final architectural point: existing ecosystems teach the same lesson. VS Code extensions declare entry points, activation events, compatibility, dependencies, capabilities, categories, and marketplace metadata in a manifest; Visual Studio uses an explicit VSIX manifest; JetBrains uses plugin.xml with version compatibility, dependencies, vendor, change notes, and even paid/freemium descriptors. LocalEndpoints.com should mirror that manifest discipline, but with stronger desktop-specific capability declarations such as filesystem, network, process launch, background tasks, and UI surface injection.
Plugin runtime, discovery, versioning, and SDK design
Recommended runtime model
At runtime, the host should never “discover” plugins by loading arbitrary assemblies into the main context. The better pattern is:
- Read a signed plugin manifest from disk or from the package index.
- Validate compatibility, signatures, capabilities, and entitlements.
- Decide whether the plugin runs in-process or out-of-process.
- If in-process, load the plugin root assembly in a collectible
AssemblyLoadContextwith anAssemblyDependencyResolver. - Activate the plugin through a stable SDK interface and keep no strong references that would block unloading. Microsoft’s plugin tutorial and unloadability guidance align directly with this pattern.
For metadata inspection before execution, use MetadataLoadContext if you must inspect assemblies without executing their code. It treats assemblies strictly as metadata and is the .NET replacement for reflection-only loading. That is useful for LocalEndpoints.com’s package validation pipeline, but it should remain a tooling step, not the primary discovery contract; the primary discovery contract should still be the explicit manifest.
SDK shape
The plugin SDK should be intentionally small and versioned. A practical minimum surface is:
ILocalEndpointsPluginIPluginContextICommandContributionISettingsPageContributionIBackgroundTaskContributionICapabilityDeclarationor manifest-based equivalents- A small eventing interface for lifecycle notifications
- DTOs for telemetry-safe diagnostics, settings, and compatibility
A minimal C# example follows. The methods include XML comments, and the DTO properties use [Display] as requested.
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Threading;
using System.Threading.Tasks;
namespace LocalEndpoints.Plugins.Abstractions;
/// <summary>
/// Defines the root contract for a LocalEndpoints.com plugin.
/// </summary>
public interface ILocalEndpointsPlugin
{
/// <summary>
/// Returns immutable metadata for the plugin instance.
/// </summary>
/// <param name="cancellationToken">A token used to cancel metadata retrieval.</param>
/// <returns>The plugin metadata.</returns>
Task<PluginMetadata> GetMetadataAsync(CancellationToken cancellationToken);
/// <summary>
/// Initializes the plugin with the host-provided context.
/// </summary>
/// <param name="context">The host services, paths, logging, and capability context.</param>
/// <param name="cancellationToken">A token used to cancel initialization.</param>
/// <returns>A task that completes when initialization finishes.</returns>
Task InitializeAsync(IPluginContext context, CancellationToken cancellationToken);
/// <summary>
/// Starts the plugin after successful initialization.
/// </summary>
/// <param name="cancellationToken">A token used to cancel startup.</param>
/// <returns>A task that completes when startup finishes.</returns>
Task StartAsync(CancellationToken cancellationToken);
/// <summary>
/// Stops the plugin and releases resources.
/// </summary>
/// <param name="cancellationToken">A token used to cancel shutdown.</param>
/// <returns>A task that completes when shutdown finishes.</returns>
Task StopAsync(CancellationToken cancellationToken);
}
/// <summary>
/// Provides services and environment details to a plugin.
/// </summary>
public interface IPluginContext
{
/// <summary>
/// Gets the plugin installation directory.
/// </summary>
string PluginDirectory { get; }
/// <summary>
/// Gets the plugin data directory.
/// </summary>
string DataDirectory { get; }
/// <summary>
/// Gets the current host version.
/// </summary>
Version HostVersion { get; }
/// <summary>
/// Gets the granted capabilities for the plugin.
/// </summary>
IReadOnlyCollection<string> GrantedCapabilities { get; }
/// <summary>
/// Publishes a host-visible notification.
/// </summary>
/// <param name="title">The notification title.</param>
/// <param name="message">The notification body.</param>
void Notify(string title, string message);
}
/// <summary>
/// Represents immutable plugin metadata.
/// </summary>
public sealed class PluginMetadata
{
[Display(Name = "plugin")]
public string PluginId { get; init; } = string.Empty;
[Display(Name = "display name")]
public string DisplayName { get; init; } = string.Empty;
[Display(Name = "version")]
public string Version { get; init; } = string.Empty;
[Display(Name = "publisher")]
public string Publisher { get; init; } = string.Empty;
[Display(Name = "description")]
public string Description { get; init; } = string.Empty;
[Display(Name = "capabilities")]
public IReadOnlyCollection<string> Capabilities { get; init; } = Array.Empty<string>();
}
Assembly loading and unloading
The host-side loader should use a custom collectible AssemblyLoadContext. This is the core Microsoft pattern for plugins and side-by-side dependencies.
using System;
using System.IO;
using System.Reflection;
using System.Runtime.Loader;
namespace LocalEndpoints.PluginHost;
/// <summary>
/// Loads a plugin and its dependencies into a collectible context.
/// </summary>
public sealed class PluginLoadContext : AssemblyLoadContext
{
private readonly AssemblyDependencyResolver _resolver;
/// <summary>
/// Creates a new plugin load context for a plugin root assembly path.
/// </summary>
/// <param name="pluginAssemblyPath">The plugin's root assembly path.</param>
public PluginLoadContext(string pluginAssemblyPath)
: base($"Plugin:{Path.GetFileNameWithoutExtension(pluginAssemblyPath)}", isCollectible: true)
{
_resolver = new AssemblyDependencyResolver(pluginAssemblyPath);
}
/// <summary>
/// Resolves managed assemblies for the plugin context.
/// </summary>
/// <param name="assemblyName">The requested assembly name.</param>
/// <returns>The resolved assembly, or null to defer resolution.</returns>
protected override Assembly? Load(AssemblyName assemblyName)
{
var assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName);
return assemblyPath is null ? null : LoadFromAssemblyPath(assemblyPath);
}
/// <summary>
/// Resolves unmanaged libraries for the plugin context.
/// </summary>
/// <param name="unmanagedDllName">The library name.</param>
/// <returns>The native library handle, or zero if not resolved.</returns>
protected override IntPtr LoadUnmanagedDll(string unmanagedDllName)
{
var dllPath = _resolver.ResolveUnmanagedDllToPath(unmanagedDllName);
return dllPath is null ? IntPtr.Zero : LoadUnmanagedDllFromPath(dllPath);
}
}
Two operational caveats matter. First, unloading is cooperative, so any static references, event handlers, background threads, timers, COM objects, or host caches that still reference plugin types can prevent collection. Second, because AssemblyLoadContext is not a security boundary, you should assume an in-process plugin can crash or compromise the process if it is malicious. Those are exactly the reasons to reserve in-process loading for trusted or verified plugins.
Manifest format
The manifest should be a first-class store object, stored both inside the package and in the store database. A JSON format is friendlier than XML for a custom platform, even though marketplaces like VSIX and JetBrains use XML. The important thing is the shape, not the serialization format: identity, version, compatibility, entry point, capabilities, dependencies, licensing, hashes, and UI metadata. Existing ecosystems consistently surface these fields.
Suggested manifest:
{
"schemaVersion": "1.0",
"pluginId": "com.localendpoints.sample.csv-export",
"displayName": "CSV Export Tools",
"publisher": {
"id": "localendpoints",
"displayName": "LocalEndpoints.com",
"email": "connect@LocalEndpoints.com",
"verified": true
},
"version": "1.4.0",
"description": "Exports selected records to CSV and scheduled archives.",
"entryAssembly": "LocalEndpoints.Sample.CsvExport.dll",
"entryType": "LocalEndpoints.Sample.CsvExport.CsvExportPlugin",
"targetFramework": "net8.0-windows",
"hostCompatibility": {
"minHostVersion": "2.2.0",
"maxHostVersionExclusive": "3.0.0"
},
"sdkCompatibility": {
"sdkPackageId": "LocalEndpoints.PluginSdk",
"versionRange": "[2.0.0,3.0.0)"
},
"capabilities": [
"ui.command",
"filesystem.readwrite:user-data",
"network:https:api.example.com"
],
"dependencies": [
{
"pluginId": "com.localendpoints.core.scheduler",
"versionRange": "[1.1.0,2.0.0)"
}
],
"licensing": {
"model": "trial-or-subscription",
"requiresOnlineValidation": true,
"offlineGracePeriodHours": 168
},
"artifacts": {
"packageFile": "com.localendpoints.sample.csv-export.1.4.0.nupkg",
"sha256": "BASE64_OR_HEX_HASH"
},
"install": {
"defaultActivation": "on-demand",
"supportsHotDisable": true
}
}
Folder and solution layout
A clean solution layout avoids coupling the shell to plugin implementation details:
/src
/LocalEndpoints.App
/LocalEndpoints.App.UI
/LocalEndpoints.Host
/LocalEndpoints.Host.Abstractions
/LocalEndpoints.PluginRuntime
/LocalEndpoints.PluginStore.Client
/LocalEndpoints.PluginSdk
/LocalEndpoints.PluginSdk.Analyzers
/LocalEndpoints.PluginSdk.Templates
/LocalEndpoints.Update.Client
/LocalEndpoints.Security
/LocalEndpoints.Telemetry
/server
/LocalEndpoints.Store.Api
/LocalEndpoints.Store.Domain
/LocalEndpoints.Store.Infrastructure
/LocalEndpoints.Store.Workers
/LocalEndpoints.Store.Contracts
/plugins
/Samples/CsvExportPlugin
/Samples/SchedulerPlugin
/TestPlugins/FaultyPlugin
/TestPlugins/NativeDependencyPlugin
/build
/scripts
/signing
/manifests
/sbom
On disk, the installed plugin layout should be deterministic and rollback-friendly:
%ProgramData%\LocalEndpoints\Plugins\
com.localendpoints.sample.csv-export\
current -> 1.4.0\
1.4.0\
plugin.json
package.sig
LocalEndpoints.Sample.CsvExport.dll
runtimes\
lib\
1.3.2\
state\
logs\
Security, trust, and isolation
The hard truth about sandboxing in modern .NET
In .NET 6/7/8, AppDomains are not the answer. Microsoft’s documentation is explicit: creating more AppDomains is unsupported in .NET 6+, there is effectively one AppDomain, and security boundaries should be provided by process boundaries. Microsoft is equally explicit that CAS is deprecated and not honored by recent .NET runtimes. So if the requirement is “third-party plugins that might be untrusted,” the host must enforce trust with separate processes or more restrictive OS isolation.
Recommended trust model
For LocalEndpoints.com, the best trust model is tiered trust, not a binary trusted/untrusted label.
| Trust tier | Typical source | Execution policy | Store treatment |
|---|---|---|---|
| First-party | LocalEndpoints.com | Can run in-process or out-of-process | Fast lane, full telemetry, auto-update |
| Verified publisher | Approved third parties with identity verification and signing | In-process only for low-risk capability sets; otherwise isolated process | Human review + automated checks |
| Unverified / private dev | Side-loaded or internal enterprise authors | Out-of-process by default; feature-limited | Not listed publicly by default |
Every plugin package should pass four checks before activation:
- Package signature valid
- Publisher trust valid
- Compatibility valid
- Entitlement valid
Use Authenticode for Windows binaries and installer artifacts, because Windows-native code signing and timestamping are well-supported through SignTool. Use NuGet package signing if the distribution artifact is .nupkg, because it gives integrity and origin guarantees. Then add a LocalEndpoints.com repository signature or manifest signature so the client can enforce “this package came through the LocalEndpoints.com store” even when the embedded package author signature is valid. Microsoft’s NuGet signing/trusted signer model is a strong template here.
Isolation options compared
| Approach | Isolation strength | Operational cost | When to use |
|---|---|---|---|
In-process AssemblyLoadContext | Low: solves dependency isolation and possible unloading, not security. | Low | Trusted, low-risk UI plugins |
| Separate process | High relative to in-process; best generic choice on Windows for plugin security boundaries. Microsoft explicitly points to separate processes for isolation in .NET 6+. | Medium | Most third-party plugins |
| AppContainer / Less-Privileged AppContainer | Higher OS-level restriction for processes; limits file/registry/resource access. | Medium to high | High-risk plugins needing tighter local restrictions |
| Windows containers, process-isolated | Better isolation than plain processes, but shares kernel in standard process isolation. | High | Enterprise and managed-plugin scenarios |
| Windows containers, Hyper-V isolated | Stronger boundary; Microsoft considers hypervisor-isolated containers a robust security boundary. | Very high | High-assurance enterprise workloads, not normal desktop UX |
| .NET on WASM/WASI | Interesting for pure compute plugins, but .NET 8 WASI was experimental and WebAssembly environments have platform/API limitations. | High today | Future niche, not core marketplace strategy |
The practical recommendation is:
- Default: out-of-process plugin host with a narrow IPC contract.
- Allow in-process only for reviewed, signed, capability-limited plugins.
- Use AppContainer for plugins that need local compute but should not touch broad user or system resources.
- Do not pitch WASM/WASI as the primary sandbox for a commercial Windows desktop extension ecosystem yet; treat it as an R&D path for deterministic compute plugins.
Supply-chain controls
A plugin store is a supply-chain problem as much as a runtime problem. LocalEndpoints.com should apply the same controls to plugins that modern package ecosystems apply to dependencies:
- Package source mapping so package IDs resolve only from intended feeds.
- Trusted signers and required signature validation for official packages.
- Dependency vulnerability auditing in CI.
- SBOM generation and retention per release.
- Static analysis and code scanning for store-hosted code or publisher-submitted source when available.
Store, licensing, UI, and update delivery
In-app plugin store UX
A credible store should behave more like a marketplace than a DLL manager. Existing plugin ecosystems show the baseline expectations: clear identity, compatibility, dependencies, categories, marketplace presentation, change notes, and commercial metadata. VS Code also exposes concepts like extension packs, dependencies, capabilities, and marketplace presentation; JetBrains exposes version compatibility, vendor, change notes, and paid/freemium descriptors. LocalEndpoints.com should adapt those patterns to a desktop host with stronger emphasis on trust and permissions.
The recommended in-app store surface has four primary views:
Search, category filters, pricing filter, “verified publisher,” host version compatibility, featured bundles, and install state.
- Catalog/search
Screenshots, description, capabilities, compatibility matrix, dependencies, change log, privacy note, support link, publisher verification badge, and “why this plugin needs these permissions.”
- Plugin details
Enable/disable, version pin, channel selection, update policy, logs, telemetry opt-in, and rollback.
- Installed plugins
Submission status, crash rate, compatibility violations, staged rollout controls, signing health, entitlements, refunds, and takedowns.
- Publisher/admin
The most important UX detail is not the search box. It is permission transparency. Every plugin detail page should show a capability panel such as:
- Filesystem:
Read/Write user-selected folders - Network:
HTTPS to api.vendor.com - Background:
Scheduled tasks - UI:
Adds commands and settings page - Process:
Launches child process
That mirrors how modern extension ecosystems disclose capability and compatibility, but it is even more important on desktop because the blast radius is larger.
Payment and licensing options
For monetization, there are three viable models:
| Option | Strengths | Weaknesses | Best fit |
|---|---|---|---|
| Stripe-backed direct commerce | Flexible subscriptions, checkout flows, webhooks, and billing APIs. | You remain merchant of record unless you add more tax/compliance services | Teams comfortable owning tax/compliance |
| Paddle merchant-of-record | Handles payments, subscriptions, taxes, refunds, chargebacks, and compliance centrally. | Less control than a fully owned billing stack | Strong default for a multi-vendor plugin marketplace |
| Microsoft Store commerce | Native Windows distribution benefits; Microsoft documents flexible monetization options, including use of own commerce for non-gaming apps. | Ties monetization strategy to Store distribution posture | Good if host app is strongly Store-centric |
For LocalEndpoints.com specifically, the most scalable marketplace pattern is usually:
- Merchant of record at platform level for public third-party plugin sales.
- Platform-issued signed entitlements to the client.
- Per-plugin trial policy controlled by the store.
- Offline grace windows for desktop reliability.
- Server-side revocation and seat enforcement for enterprise plans.
Supported commercial models should include:
- Free
- Free with optional paid upgrade
- Trial then subscription
- Per-user subscription
- Per-device seat
- Perpetual license with maintenance window
- Site license / enterprise private distribution
Main app updates and plugin updates
There are two separate update problems: the host update and the plugin update.
For the host app, if you package the Windows application as MSIX/App Installer, Windows gives you strong install/uninstall behavior, automatic updates, and differential updates based on block maps. App Installer also supports update configuration and hosting from web, network share, or local share. This is the cleanest Windows-native path if packaged distribution is acceptable.
For the plugin ecosystem, there are three realistic strategies:
| Strategy | Best for | Pros | Cons |
|---|---|---|---|
| MSIX optional/related packages | First-party modules in tightly controlled Windows environments | Native Windows packaging and update semantics. | Awkward for broad third-party marketplace workflows |
| NuGet-style package + LocalEndpoints API | Broad plugin ecosystem | Familiar packaging, easy CI/CD, good author experience, easy custom metadata | You must build the marketplace, policy, and updater yourself |
| Squirrel-style packages | Unpackaged app scenarios | Mature Windows update ecosystem, delta packages, channels, simple HTTP distribution. | Less aligned with NuGet-compatible authoring and cryptographic policy depth |
My recommendation is:
- Host app: MSIX/App Installer if your distribution model allows it.
- Plugins:
.nupkgor.lepluginpackage format with LocalEndpoints-specific metadata and signatures. - Updater: keep plugin updates inside the LocalEndpoints.com store client, not delegated to NuGet UI.
Delta updates, rollback, and compatibility
The update strategy should combine full packages, optional delta packages, and a two-slot rollback model.
| Update mode | Recommendation |
|---|---|
| Full package replace | Always support it as the safe fallback |
| Delta package | Use for popular large plugins and the host app when bandwidth matters |
| Staged rollout | Essential for public releases |
| Auto-update | Default on for verified plugins, with admin policy override |
| Rollback | Keep at least previous known-good version locally |
Sample plugin install/update lifecycle:
sequenceDiagram
participant C as Desktop Client
participant A as Store API
participant P as Package CDN
participant L as License Service
C->>A: GET /v1/updates/check?host=2.4.0&channel=stable
A-->>C: update manifest and rollout decision
C->>L: POST /v1/licenses/validate
L-->>C: signed entitlement and policy
C->>P: GET plugin package and signatures
C->>C: verify signatures, hashes, compatibility
C->>C: install to versioned folder
C->>C: health-check activation
alt activation succeeds
C->>C: switch current symlink/pointer
C->>A: POST install success telemetry
else activation fails
C->>C: revert to previous version
C->>A: POST rollback telemetry
end
Recommended API, schema, and interaction flows
API design
The store should expose two API surfaces:
- A marketplace/control API for search, metadata, licensing, rollout, reviews, telemetry, and admin.
- Optionally, a NuGet-compatible service index if you want author tooling or internal operations to reuse NuGet flows and clients. NuGet’s own server model begins from a service index and exposes HTTP resources for search, metadata, publishing, and download; that can be useful as an internal compatibility layer, but it is not sufficient as the public marketplace API by itself.
Recommended public endpoints:
GET /v1/catalog/search?q=&category=&pricing=&hostVersion=&skip=&take=
GET /v1/catalog/plugins/{pluginId}
GET /v1/catalog/plugins/{pluginId}/versions
GET /v1/catalog/plugins/{pluginId}/versions/{version}
GET /v1/catalog/plugins/{pluginId}/reviews
POST /v1/install-tickets
POST /v1/licenses/validate
GET /v1/updates/check?hostVersion=&channel=&installedPluginId=&installedVersion=
POST /v1/telemetry/events
POST /v1/telemetry/crashes
POST /v1/reviews
Recommended authenticated publisher/admin endpoints:
POST /v1/publisher/plugins
POST /v1/publisher/plugins/{pluginId}/versions
POST /v1/publisher/plugins/{pluginId}/artifacts
POST /v1/publisher/plugins/{pluginId}/submit
POST /v1/publisher/plugins/{pluginId}/rollouts
POST /v1/publisher/plugins/{pluginId}/rollback
GET /v1/publisher/plugins/{pluginId}/validation-report
GET /v1/admin/moderation/queue
POST /v1/admin/moderation/{submissionId}/approve
POST /v1/admin/moderation/{submissionId}/reject
A sample update-check request and response:
POST /v1/updates/check
{
"client": {
"appId": "LocalEndpoints.Desktop",
"appVersion": "2.4.0",
"channel": "stable",
"os": "windows-11",
"arch": "x64"
},
"installedPlugins": [
{
"pluginId": "com.localendpoints.sample.csv-export",
"version": "1.3.2"
}
]
}
{
"hostUpdate": {
"available": true,
"version": "2.4.1",
"downloadUrl": "https://cdn.localendpoints.com/host/2.4.1/LocalEndpoints.msixbundle",
"sha256": "HASH",
"mandatory": false
},
"pluginUpdates": [
{
"pluginId": "com.localendpoints.sample.csv-export",
"currentVersion": "1.3.2",
"targetVersion": "1.4.0",
"rolloutState": "eligible",
"packageUrl": "https://cdn.localendpoints.com/plugins/csv-export/1.4.0/package.nupkg",
"signatureUrl": "https://cdn.localendpoints.com/plugins/csv-export/1.4.0/package.sig",
"minHostVersion": "2.2.0",
"maxHostVersionExclusive": "3.0.0"
}
]
}
Database schema
A relational store schema is the right default because you need transactions, moderation states, versions, entitlements, and analytics dimensions. Example tables:
CREATE TABLE dbo.Publishers
(
PublisherId UNIQUEIDENTIFIER NOT NULL PRIMARY KEY,
DisplayName NVARCHAR(200) NOT NULL,
Email NVARCHAR(320) NOT NULL,
VerificationStatus NVARCHAR(50) NOT NULL,
CreatedUtc DATETIME2 NOT NULL CONSTRAINT DF_Publishers_CreatedUtc DEFAULT SYSUTCDATETIME(),
UpdatedUtc DATETIME2 NOT NULL CONSTRAINT DF_Publishers_UpdatedUtc DEFAULT SYSUTCDATETIME()
);
CREATE TABLE dbo.Plugins
(
PluginId NVARCHAR(200) NOT NULL PRIMARY KEY,
PublisherId UNIQUEIDENTIFIER NOT NULL,
DisplayName NVARCHAR(200) NOT NULL,
Summary NVARCHAR(500) NOT NULL,
DescriptionMarkdown NVARCHAR(MAX) NOT NULL,
Category NVARCHAR(100) NOT NULL,
PricingModel NVARCHAR(50) NOT NULL,
IsListed BIT NOT NULL,
CreatedUtc DATETIME2 NOT NULL CONSTRAINT DF_Plugins_CreatedUtc DEFAULT SYSUTCDATETIME(),
UpdatedUtc DATETIME2 NOT NULL CONSTRAINT DF_Plugins_UpdatedUtc DEFAULT SYSUTCDATETIME(),
CONSTRAINT FK_Plugins_Publishers FOREIGN KEY (PublisherId) REFERENCES dbo.Publishers(PublisherId)
);
CREATE TABLE dbo.PluginVersions
(
PluginVersionId UNIQUEIDENTIFIER NOT NULL PRIMARY KEY,
PluginId NVARCHAR(200) NOT NULL,
VersionSemVer NVARCHAR(50) NOT NULL,
MinHostVersion NVARCHAR(50) NOT NULL,
MaxHostVersionExclusive NVARCHAR(50) NULL,
PackageUrl NVARCHAR(1000) NOT NULL,
PackageSha256 NVARCHAR(128) NOT NULL,
SignatureUrl NVARCHAR(1000) NOT NULL,
ManifestJson NVARCHAR(MAX) NOT NULL,
ReviewStatus NVARCHAR(50) NOT NULL,
RolloutPercent INT NOT NULL,
PublishedUtc DATETIME2 NULL,
CreatedUtc DATETIME2 NOT NULL CONSTRAINT DF_PluginVersions_CreatedUtc DEFAULT SYSUTCDATETIME(),
CONSTRAINT UQ_PluginVersions UNIQUE (PluginId, VersionSemVer),
CONSTRAINT FK_PluginVersions_Plugins FOREIGN KEY (PluginId) REFERENCES dbo.Plugins(PluginId)
);
CREATE TABLE dbo.PluginDependencies
(
PluginDependencyId UNIQUEIDENTIFIER NOT NULL PRIMARY KEY,
PluginVersionId UNIQUEIDENTIFIER NOT NULL,
DependencyPluginId NVARCHAR(200) NOT NULL,
VersionRange NVARCHAR(100) NOT NULL,
IsOptional BIT NOT NULL,
CONSTRAINT FK_PluginDependencies_PluginVersions FOREIGN KEY (PluginVersionId) REFERENCES dbo.PluginVersions(PluginVersionId)
);
CREATE TABLE dbo.Entitlements
(
EntitlementId UNIQUEIDENTIFIER NOT NULL PRIMARY KEY,
AccountId UNIQUEIDENTIFIER NOT NULL,
PluginId NVARCHAR(200) NOT NULL,
LicenseType NVARCHAR(50) NOT NULL,
Status NVARCHAR(50) NOT NULL,
SeatsPurchased INT NOT NULL,
SeatsAssigned INT NOT NULL,
ValidFromUtc DATETIME2 NOT NULL,
ValidToUtc DATETIME2 NULL,
IssuedUtc DATETIME2 NOT NULL CONSTRAINT DF_Entitlements_IssuedUtc DEFAULT SYSUTCDATETIME()
);
CREATE TABLE dbo.InstallEvents
(
InstallEventId UNIQUEIDENTIFIER NOT NULL PRIMARY KEY,
AccountId UNIQUEIDENTIFIER NULL,
DeviceId NVARCHAR(200) NOT NULL,
PluginId NVARCHAR(200) NOT NULL,
PluginVersion NVARCHAR(50) NOT NULL,
EventType NVARCHAR(50) NOT NULL,
Success BIT NOT NULL,
CorrelationId UNIQUEIDENTIFIER NOT NULL,
OccurredUtc DATETIME2 NOT NULL CONSTRAINT DF_InstallEvents_OccurredUtc DEFAULT SYSUTCDATETIME()
);
Suggested supporting tables: Accounts, Devices, Rollouts, Reviews, CrashReports, TelemetryDailyAggregates, PublisherKeys, Revocations, and SubmissionAuditTrail.
Client-server interaction flows
The most important flows are install, licensed activation, and safe rollback.
Install flow
- Client searches catalog.
- Client requests plugin details and compatibility.
- Client requests an install ticket.
- Store checks entitlements, policy, rollout eligibility, and trust.
- Client downloads package + signature.
- Client verifies hash + signatures.
- Client installs to versioned folder.
- Client activates in sandbox policy.
- Client reports success/failure telemetry.
License validation flow
- Client sends device/account/plugin/version.
- Server returns signed entitlement JWT or similar signed token.
- Client caches token with expiry and policy hints.
- Plugin activation checks local cache first, then refreshes if needed.
- Offline grace window applies.
- Revocation list or next online check disables future use if revoked.
Rollback flow
- New plugin version is installed side-by-side.
- Client launches health probe.
- If initialization, activation, or policy validation fails, client flips the active pointer back to the previous version.
- Crash threshold or startup-failure threshold can also trigger automatic rollback.
Operations, testing, legal considerations, and roadmap
CI/CD and package hosting
LocalEndpoints.com should support three hosting modes:
| Hosting mode | Use case | Recommendation |
|---|---|---|
| nuget.org | Public SDK/support packages for authors | Good for SDKs, analyzers, templates, not ideal as the only marketplace backend. |
| Private feed such as Azure Artifacts or GitHub Packages | Internal packages, prerelease feeds, secure supply chain | Strong for publisher pipelines and private channels. |
| Custom LocalEndpoints.com store | Listed marketplace, policy, licensing, telemetry, rollouts | Required for the actual plugin store |
A strong publisher pipeline is:
- Build
- Unit/integration tests
- API compatibility checks
- Package creation
- Signing
- SBOM generation
- Vulnerability audit
- Malware/static analysis
- Publish to staging feed
- Automated validation in a real host
- Human moderation for public release
- Progressive rollout
For CI/CD, GitHub Actions and Azure DevOps both work well. GitHub’s .NET workflows, NuGet trusted publishing, and Azure Artifacts flows are mature building blocks. Use short-lived credentials and OIDC where possible, not long-lived secrets.
Telemetry and analytics
Observability should use OpenTelemetry as the common schema for logs, metrics, and traces, with export either to Azure Monitor/Application Insights or another OTLP backend. Microsoft’s modern guidance centers observability on OpenTelemetry, and Application Insights now integrates directly with that model. Use telemetry to drive product and operational views separately: install success, activation failures, update conversion, rollback rate, crash rate by plugin version, latency by plugin capability, and feature adoption by plugin category.
Do not let plugin authors emit unrestricted raw telemetry through the platform channel. The host should mediate telemetry with:
- schema validation
- PII redaction
- event quotas
- user opt-in/opt-out
- tenant policy controls
- publisher-specific namespaces
Performance and security testing
The platform should have a dedicated validation matrix beyond normal desktop app testing:
| Test area | Why it matters | Tools and practices |
|---|---|---|
| Plugin load/unload regression tests | Ensures collectible ALCs actually unload and don’t leak | Automated unload tests, heap/dump analysis. |
| API compatibility tests | Prevents silent SDK breakage for authors | ApiCompat and strict compatibility gates. |
| Performance benchmarks | Measures command latency, memory overhead, cold-start cost | BenchmarkDotNet and real-world scenario tests. |
| Integration tests | Verifies end-to-end store/update/install flows | .NET integration testing patterns. |
| Dependency/security audits | Protects the supply chain | NuGet auditing, source mapping, code scanning, SBOM. |
Two test cases deserve special attention because plugin platforms often miss them:
- Unloadability tests after repeated enable/disable cycles.
- Rollback correctness tests after partial update failure, expired entitlement, or revoked signing keys.
Legal, terms, and privacy
A plugin marketplace needs more than a EULA. At minimum, LocalEndpoints.com should maintain:
- End-user terms for the host application
- Marketplace terms governing plugin purchases and refunds
- Publisher agreement covering content, takedowns, malware, support duties, and indemnities
- Privacy notice for telemetry, purchase, account, and crash data
- Data processing terms if you serve organizational customers
- Moderation/takedown policy
- Export controls/sanctions compliance process
- Tax handling policy aligned to your payment model
On privacy, the regulatory through-line is consistent: users must be told what is collected, why, how long it is kept, and with whom it is shared. UK GDPR guidance emphasizes the right to be informed and required privacy information; EU guidance emphasizes lawfulness, transparency, purpose limitation, and privacy by design; California guidance emphasizes notice obligations for covered businesses. For a plugin store, that means layered notices are ideal: a short in-app privacy summary with links to detailed policy, plus plugin-specific disclosures on each plugin detail page.
Implementation roadmap
A realistic roadmap for a rigorous first release is:
| Phase | Focus | Milestones |
|---|---|---|
| Discovery and platform design | Contracts, trust model, packaging, UI framework choice | Architecture decision records, manifest schema, threat model |
| Host foundation | Desktop shell, Generic Host, DI, logging, settings, telemetry | App shell running with internal plugins |
| Plugin runtime | Manifest discovery, ALC loading, out-of-proc runner, capability resolver | Trusted and isolated plugin execution working |
| Store MVP | Catalog, package upload, signing, search, install, update check | Private marketplace working end to end |
| Commercialization | Accounts, entitlements, billing, trials, receipts | Paid plugins in private beta |
| Hardening | Rollbacks, staged rollouts, moderation, security scanning, SBOM | Public beta with verified publishers |
| General availability | Publisher portal, analytics, documentation, support workflows | Stable public launch under LocalEndpoints.com |
A reasonable milestone cadence for a greenfield project is roughly:
- Months 1–2: architecture, SDK, shell foundation
- Months 3–4: runtime, manifests, plugin install/update core
- Months 5–6: store API, package signing, private feed, telemetry
- Months 7–8: licensing/payments, rollback, staging, moderation
- Months 9–10: publisher experience, docs, security hardening, beta
- Months 11–12: GA, analytics, operational tuning
Final recommendation
The most defensible overall design for LocalEndpoints.com is:
- Host on
net8.0-windowsonly if constrained to 6/7/8, but prepare to move to current LTS quickly. - Use manifest-first plugins with a small stable SDK.
- Load trusted plugins with collectible
AssemblyLoadContext. - Run untrusted or privileged plugins out of process, optionally tightened with AppContainer.
- Sign everything and enforce publisher trust + repository trust + entitlement trust.
- Build a custom LocalEndpoints.com marketplace API even if you reuse NuGet-compatible packaging.
- Use MSIX/App Installer for the host update path when feasible, and versioned side-by-side plugin installs with rollback for plugins.
- Use OpenTelemetry for observability, NuGet audit/source mapping/signing for supply-chain hygiene, and API compatibility gates to protect plugin authors from host churn.
This balances extensibility, Windows-native UX, commercial viability, and the most important non-negotiable fact in modern .NET plugin systems: dependency isolation is easy; security isolation is not, unless you move risky code out of process.