Understanding Dalamud’s IoC container and service injection system
Dalamud uses a sophisticated Inversion of Control (IoC) container to manage services and inject dependencies into plugins. This system ensures clean separation of concerns and makes testing easier.
// From ServiceContainer.cs:184if (serviceType.GetCustomAttribute<ServiceManager.ScopedServiceAttribute>() != null){ if (scope == null) { throw new InvalidOperationException( $"Failed to create {serviceType.FullName ?? serviceType.Name}, is scoped but no scope provided"); } return await scope.CreatePrivateScopedObject(serviceType, scopedObjects);}
Plugin-specific services like DalamudPluginInterface are scoped to each plugin instance.
Services can be resolved by interface or concrete type:
// Both of these work:public MyPlugin(IClientState clientState) { } // Interfacepublic MyPlugin(ClientState clientState) { } // Concrete type (not recommended)
The container maintains a mapping:
// From ServiceContainer.cs:68public void RegisterInterfaces(Type type){ var resolveViaTypes = type .GetCustomAttributes() .OfType<ResolveViaAttribute>() .Select(x => x.GetType().GetGenericArguments().First()); foreach (var resolvableType in resolveViaTypes) { this.interfaceToTypeMap[resolvableType] = type; }}
Always use interfaces (like IClientState) rather than concrete types. This future-proofs your code against internal Dalamud changes.
Plugins can inject any service marked with [PluginInterface]:
// From Dalamud/IoC/PluginInterfaceAttribute.cs:7[AttributeUsage(AttributeTargets.Class)]public class PluginInterfaceAttribute : Attribute{}
Commonly used services include:
DalamudPluginInterface
Core plugin interface providing access to UI builder, configuration, and plugin metadata:
public MyPlugin(DalamudPluginInterface pluginInterface){ pluginInterface.UiBuilder.Draw += DrawUI; var configDir = pluginInterface.ConfigDirectory;}
IClientState
Access player and character state:
public MyPlugin(IClientState clientState){ var player = clientState.LocalPlayer; var territory = clientState.TerritoryType; clientState.TerritoryChanged += OnTerritoryChanged;}
ICommandManager
Register chat commands:
public MyPlugin(ICommandManager commands){ commands.AddHandler("/mycommand", new CommandInfo(OnCommand) { HelpMessage = "My command" });}
IChatGui
Print messages to chat:
public MyPlugin(IChatGui chat){ chat.Print("Hello from my plugin!");}
IDataManager
Access game data:
public MyPlugin(IDataManager dataManager){ var item = dataManager.GetExcelSheet<Item>()?.GetRow(1);}
IFramework
Subscribe to game update ticks:
public MyPlugin(IFramework framework){ framework.Update += OnFrameworkUpdate;}
For a complete list of services, see the API Reference.
// In your tests, you can provide mock servicesvar mockCommands = new Mock<ICommandManager>();var mockChat = new Mock<IChatGui>();var plugin = new MyPlugin( mockPluginInterface.Object, mockCommands.Object, mockChat.Object);// Verify behaviormockCommands.Verify(x => x.AddHandler("/mycommand", It.IsAny<CommandInfo>()), Times.Once);