Note
Access to this page requires authorization. You can try signing in or changing directories.
Access to this page requires authorization. You can try changing directories.
Windows provides several technologies that let your app provide functionality to other apps or consume third-party add-ins. This article compares the available extensibility options for Windows App SDK desktop apps.
Extensibility options overview
| Technology | Description | Package identity required | Minimum OS |
|---|---|---|---|
| App services | Request/response communication between apps via AppServiceConnection |
Yes | Windows 10 1607 |
| App extensions | Plug-in model — host app discovers content from extension packages | Yes | Windows 10 1607 |
| Package extensions | Broader package-level extensibility with uap17:PackageExtension |
Yes | Windows 11 |
| Optional packages | Additional content packages that supplement a main app | Yes | Windows 10 1709 |
| Resource packages | Language, scale, and accessibility assets separated by market | Yes | Windows 10 |
Choosing the right technology
Use app services when
- You need two-way communication between separate apps.
- The consumer app sends a request and waits for a response.
- You want to expose an API-like interface to other apps.
Example: A translation service that other apps can call to translate text.
Use app extensions when
- Your app needs a plug-in model where third parties provide content, themes, or add-ins.
- Extensions are discovered at runtime from installed packages.
- Extensions provide data or configuration, not executable code (code execution should use app services).
Example: An image editor that discovers filter packs from installed extension packages.
Use package extensions when
- You need broader package-level extensibility on Windows 11.
- Extensions need access to more package content than the
PublicFoldermodel allows.
Use optional packages when
- You have additional content (DLC, premium features) distributed as separate packages.
- Content is authored by the same publisher.
Architecture patterns
App service with extension discovery
Combine app extensions with app services for a full plug-in architecture:
- Your host app uses
AppExtensionCatalogto discover installed extensions. - Each extension declares properties that describe its capabilities.
- When the user activates an extension, the host app connects to the extension's app service for two-way communication.
┌─────────────────┐ ┌──────────────────┐
│ Host app │ │ Extension app │
│ │ │ │
│ AppExtension │◄────►│ AppExtension │
│ Catalog │ │ declaration │
│ │ │ │
│ AppService │◄────►│ AppService │
│ Connection │ │ provider │
└─────────────────┘ └──────────────────┘
Content extension only
For simpler scenarios where extensions provide static content (themes, templates, data files):
- The host app discovers extensions through
AppExtensionCatalog. - It reads files from the extension's
PublicFolder. - No app service is needed.
Differences from UWP extensibility
The extensibility technologies described here work the same way in Windows App SDK desktop apps as they do in UWP, with one requirement: MSIX package identity. All extensibility features rely on the package manifest for declarations and the package catalog for discovery.
If your desktop app is unpackaged, you cannot use these extensibility technologies. Consider alternative approaches such as:
- COM-based plug-in interfaces
- File-system-based extension discovery
- Named pipes or other IPC mechanisms
File-based plugin discovery for unpackaged apps
For unpackaged WinUI 3 apps, you can implement a plugin system using .NET's AssemblyLoadContext to load extensions from a known folder:
public class PluginLoader
{
private readonly string _pluginDirectory;
public PluginLoader(string pluginDirectory)
{
_pluginDirectory = pluginDirectory;
}
public IEnumerable<T> LoadPlugins<T>() where T : class
{
if (!Directory.Exists(_pluginDirectory))
yield break;
foreach (var dll in Directory.GetFiles(_pluginDirectory, "*.dll"))
{
var context = new PluginLoadContext(dll);
var assembly = context.LoadFromAssemblyPath(Path.GetFullPath(dll));
foreach (var type in assembly.GetTypes()
.Where(t => typeof(T).IsAssignableFrom(t) && !t.IsAbstract))
{
if (Activator.CreateInstance(type) is T plugin)
yield return plugin;
}
}
}
}
// Custom AssemblyLoadContext to isolate plugin dependencies
public class PluginLoadContext : AssemblyLoadContext
{
private readonly AssemblyDependencyResolver _resolver;
public PluginLoadContext(string pluginPath) : base(isCollectible: true)
{
_resolver = new AssemblyDependencyResolver(pluginPath);
}
protected override Assembly? Load(AssemblyName assemblyName)
{
var path = _resolver.ResolveAssemblyToPath(assemblyName);
return path != null ? LoadFromAssemblyPath(path) : null;
}
}
Warning
Loading assemblies from disk without validation is a security risk. In production, verify assembly signatures (such as Authenticode) before loading, restrict the plugin directory's ACL permissions, and consider running plugins in a separate process with reduced privileges.
Define a shared interface contract in a separate assembly that both the host and plugins reference:
// Contoso.App.Contracts (shared assembly)
public interface IPluginExtension
{
string Name { get; }
string Description { get; }
void Execute(IServiceProvider services);
}
Note
Using isCollectible: true in the AssemblyLoadContext allows you to unload plugins at runtime. This approach avoids the versioning issues that MEF (Managed Extensibility Framework) can introduce in desktop apps.
Related content
Windows developer