Extension Manager and Plugin Architecture¶
Merchello uses a modular plugin system built around the ExtensionManager. This is how your custom providers, strategies, and resolvers get discovered and instantiated at runtime -- without you needing to write any registration boilerplate.
How It Works¶
When your Umbraco site starts up and calls AddMerchello(), Merchello scans loaded assemblies for classes that implement any of its provider interfaces. If it finds yours, your code gets automatically loaded and made available.
Here's the simplified flow:
App Starts
-> AddMerchello() called in Startup
-> DiscoverProviderAssemblies() scans loaded assemblies
-> AssemblyManager caches the discovered types
-> Provider managers use ExtensionManager to instantiate them on demand
What Gets Discovered¶
Merchello scans for implementations of these interfaces (see DiscoverProviderAssemblies() in Startup.cs for the authoritative list):
| Interface | Purpose |
|---|---|
IPaymentProvider |
Payment gateways (Stripe, PayPal, etc.) |
IShippingProvider |
Shipping rate providers (FedEx, UPS, flat-rate) |
ITaxProvider |
Tax calculation providers (Avalara, manual rates) |
IFulfilmentProvider |
3PL fulfilment providers (ShipBob, Supplier Direct) |
IExchangeRateProvider |
Currency exchange rate sources |
IAddressLookupProvider |
Address autocomplete/validation providers |
IOrderGroupingStrategy |
Custom order grouping strategies |
ICommerceProtocolAdapter |
Commerce protocol adapters (UCP) |
IProductFeedValueResolver |
Custom product feed field resolvers |
IMerchelloAction |
Custom checkout/invoice actions (see MerchelloActions.md) |
IHealthCheck |
Custom health checks surfaced in the backoffice |
IEmailAttachment |
Custom email attachment providers |
Registering Your Plugin Assembly¶
There are two ways to get your plugin assembly discovered:
Option 1: Automatic Discovery (Recommended)¶
If your assembly is loaded in the app domain and contains a class implementing any of the interfaces above, Merchello finds it automatically. This is the simplest approach -- just reference your NuGet package or project and you're done.
// Your custom provider in a separate project/NuGet package
// As long as it's referenced by the web project, it gets found automatically
public class AcmeShippingProvider : ShippingProviderBase
{
// ...
}
Option 2: Explicit Assembly Registration¶
If automatic discovery doesn't pick up your assembly (for example, if it's loaded dynamically), you can pass it explicitly:
// In your Startup.cs / Program.cs
builder.CreateUmbracoBuilder()
.AddMerchello(pluginAssemblies: [typeof(AcmeShippingProvider).Assembly])
.Build();
How ExtensionManager Instantiates Providers¶
The ExtensionManager uses ActivatorUtilities.CreateInstance() from the DI container. This means your providers can use constructor injection for any registered service:
public class AcmeShippingProvider(
ICurrencyService currencyService,
IHttpClientFactory httpClientFactory,
ILogger<AcmeShippingProvider> logger)
: ShippingProviderBase(currencyService)
{
// All dependencies are resolved from the DI container
}
Warning: Always use constructor injection. Setter injection, service locator calls, and post-construction configuration hooks are not supported -- Merchello relies on
ActivatorUtilitiesto create instances, so all dependencies must be constructor parameters. IfMerchello.Coreneeds a dependency implemented in a web project, define the interface inMerchello.Coreand register the concrete type via the host's DI container during startup.
Assembly Scanning Details¶
When AddMerchello() runs, it builds a list of assemblies to scan:
- The web project assembly (
typeof(Startup).Assembly) - The Merchello.Core assembly (
typeof(MerchelloDbContext).Assembly) - Any explicitly passed
pluginAssemblies - Assemblies discovered by scanning
AppDomain.CurrentDomain.GetAssemblies()
System and framework assemblies (System.*, Microsoft.*, netstandard, mscorlib) are skipped for performance.
The discovered assemblies are registered with AssemblyManager.SetAssemblies(), which makes them available to all ExtensionManager calls throughout the application lifetime.
Provider Manager Pattern¶
Each provider type has its own "manager" that wraps ExtensionManager and adds provider-specific logic (configuration loading, enabling/disabling, saved settings, etc.). For example:
PaymentProviderManagermanagesIPaymentProviderinstances.ShippingProviderManagermanagesIShippingProviderinstances.TaxProviderManagermanagesITaxProviderinstances.FulfilmentProviderManager,ExchangeRateProviderManager,AddressLookupProviderManager,CommerceProtocolManagerfollow the same pattern.
When a manager needs provider instances, it calls something like:
The useCaching flag tells ExtensionManager to cache the discovered types so subsequent calls don't re-scan assemblies.
Enabling and Disabling Providers¶
Discovery is mechanical -- a matching class is always found by the scan. Whether a provider is actually used at runtime is a separate, per-manager decision:
- Payment / shipping / tax / fulfilment / address lookup providers store an enabled flag in their provider-setting DTO (e.g.,
PaymentProviderSettingDto,ShippingProviderSetting), toggled from the backoffice. - Exchange rate providers are singular -- only one is active at a time, selected via
IExchangeRateProviderManager.SetActiveProviderAsync. IOrderGroupingStrategyis chosen viaappsettings.json:
ICommerceProtocolAdapterexposes its ownIsEnabledflag driven by configuration (see theProtocolSettings.EnabledProtocolslist).
Removing a class from the build removes it from discovery entirely; disabling via settings leaves it discovered but unused.
Caching Behavior¶
ExtensionManager supports two levels of caching:
- Type caching (
useCaching: true): Caches the discoveredTypeobjects so assembly scanning only happens once per interface. - Instance creation: New instances are created each time
GetInstancesis called. The provider managers handle their own instance caching.
Tip: If you update your provider class and redeploy, the type cache is cleared automatically since the app restarts. No manual cache invalidation needed.
Creating Your Own Provider¶
The general pattern for any provider is:
- Create a class that extends the appropriate base class (e.g.,
PaymentProviderBase,ShippingProviderBase) - Implement the required abstract members (typically
Metadataand a few core methods) - Use constructor injection for dependencies
- Reference your assembly from the web project
That's it. See the individual provider guides for detailed walkthroughs:
- Creating Payment Providers
- Creating Shipping Providers
- Creating Tax Providers
- Creating Fulfilment Providers
- Creating Exchange Rate Providers
- Creating Address Lookup Providers
- Custom Order Grouping Strategies
- Creating Product Feed Resolvers
- Custom Notification Handlers
- Creating Commerce Protocol Adapters