Step-by-step guide for adding a new module as a plugin: folder structure, plugin registration in Program.cs, module initialization, and build-order dependencies in AppInfraDemo.sln.
A plugin is one isolated AssemblyLoadContext.
It has one primary assembly (the first argument in .AddPlugin(...)) and zero or more co-loaded assemblies (additional arguments in .AddPlugin(...) that are not referenced by any other assembly).
Logically, may contain more modules if multiple IModule implementations are present, but we follow a one-plugin-per-module convention for simplicity.
Follow the steps below in order when adding a new module.
Create the standard sub-projects under Modules/{Module}/:
Modules/{Module}/
├─ {Module}.DataModel/ # Entities only — no logic, no EF references
├─ {Module}.Services/ # Business logic — [Service] attribute for DI
├─ {Module}.DbContext/ # EF DbContext (scaffolded/generated)
└─ {Module}.ConsoleCommands/ # Optional — IConsoleCommand implementations
Project configuration rules:
{Module}.Services, {Module}.ConsoleCommands → set <EnableDynamicLoading>true</EnableDynamicLoading>{Module}.DbContext → use <PrivateAssets>all</PrivateAssets> on EF packages (prevents leaking EF to Services){Module}.DataModel → standard class library, no special flagsRegister the new module in UI/ConsoleUi/Program.cs via .AddPlugin():
options
.AddPlugin("Sales.Services", "Sales.DbContext", "Sales.ConsoleCommands")
.AddPlugin("Notifications.Services")
.AddPlugin("{Module}.Services", "{Module}.DbContext", "{Module}.ConsoleCommands"); // new
Rules:
<EnableDynamicLoading>true</EnableDynamicLoading>LoadContext that are not referenced by any other assembly; must also have <EnableDynamicLoading>true</EnableDynamicLoading>LoadContext is created per .AddPlugin(...){ModuleName}.{AssemblySuffix} — Modules/ and Infra/ folders are physical only, not part of the namespaceImplement IModule in {Module}.Services for startup logic:
[Service(typeof(IModule), ServiceLifetime.Singleton)]
internal sealed class {Module}ServicesModule(INotificationService notificationService) : IModule
{
public void Initialize(IHost host)
{
notificationService.NotifyAlive(this);
}
}
Initialize() is called once at app startup from Main()Contracts interfaces (no cross-module service types)By default the order of IModule.Initialize() calls is non-deterministic.
To control the order use [Priority(int)] attribute from AppBoot/DependencyInjection on the IModule implementation.
Plugin assemblies have <EnableDynamicLoading>true</EnableDynamicLoading> and are not referenced directly, so dotnet build AppInfraDemo.sln skips them unless build-order dependencies are declared explicitly.
Add ProjectSection(ProjectDependencies) = postProject blocks inside the relevant Project(...) entries in AppInfraDemo.sln.
ConsoleUi.AddPlugin(...)) must be declared as a Project Dependency of the primary assemblyConsoleUi → primary plugin assemblies
Project("{FAE04EC0-...}") = "ConsoleUi", "UI\ConsoleUi\ConsoleUi.csproj", "{GUID-ConsoleUi}"
ProjectSection(ProjectDependencies) = postProject
{GUID-Sales.Services} = {GUID-Sales.Services}
{GUID-Notifications.Services} = {GUID-Notifications.Services}
{GUID-ProductsManagement.Services} = {GUID-ProductsManagement.Services}
{GUID-PersonsManagement.Services} = {GUID-PersonsManagement.Services}
{GUID-Export.Services} = {GUID-Export.Services}
EndProjectSection
EndProject
Primary plugin assembly → its co-loaded assemblies
Project("{FAE04EC0-...}") = "Sales.Services", "Modules\Sales\Sales.Services\Sales.Services.csproj", "{GUID-Sales.Services}"
ProjectSection(ProjectDependencies) = postProject
{GUID-Sales.DbContext} = {GUID-Sales.DbContext}
{GUID-Sales.ConsoleCommands} = {GUID-Sales.ConsoleCommands}
EndProjectSection
EndProject
If a plugin has no co-loaded assemblies (e.g., .AddPlugin("Notifications.Services")), no ProjectDependencies block is needed on that project.
GUIDs are on the Project(...) line of each entry in AppInfraDemo.sln:
Project("{FAE04EC0-...}") = "Sales.DbContext", "Modules\Sales\Sales.DbContext\...", "{8ECFDB0C-9146-4C51-B8AF-3DC696492DAE}"
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
use this GUID in ProjectDependencies
Right-click the solution → Project Build Dependencies → select the dependant project and tick its dependencies. This writes the same ProjectSection(ProjectDependencies) blocks automatically.