Table of Contents

Services and dependency injection

A game's services live in one dependency injection container, from Microsoft.Extensions.DependencyInjection. The engine registers its own services in the same container, so a game gets them the same way it gets its own: by constructor injection, or by asking the game for them with GetService<T>().

Registering services

Register a game's services in Configure. The method is called once, first thing in Run(), and it is given the collection the container is built from:

protected override void Configure(IServiceCollection services)
{
  services.AddSingleton<SaveGame>();
  services.AddTransient<IEnemyFactory, EnemyFactory>();
}

There is no container yet inside Configure, so do not resolve anything there. Resolve from Initialize onwards.

The registration methods of the other packages, such as AddUI() and AddShaderCompiler(), are called the same way, from Configure, alongside the game's own.

Resolving services

Inside the game, ask for a service with GetService<T>():

protected override void Initialize()
{
  var saves = GetService<SaveGame>();
  base.Initialize();
}

GetService<T>() returns null if T is not registered. It works from Initialize onwards.

Any class the container creates gets its dependencies through its constructor. Ask for a service there, rather than resolving it inside the class:

public class EnemySpawner
{
  private readonly IContentManager _content;

  public EnemySpawner(IContentManager content)
  {
    _content = content;
  }
}

Register EnemySpawner with services.AddSingleton<EnemySpawner>() and it receives the engine's content manager. A class you create with new gets no injected dependencies; pass them yourself, or resolve it through the container.

Lifetimes

The container is built with scope validation on, and the game resolves everything from its root provider. Register game services as singletons or transients: a scoped service resolved from the root throws. Most of the engine's services are singletons.

CreateServiceProvider(IServiceCollection) builds the provider from the collection. The default returns services.BuildServiceProvider(true). Override it to build the provider another way, such as with a different container. The provider must resolve every registration in the collection, including the engine's.

What the engine registers

These types are in the container, so a game's classes can take them as constructor parameters or resolve them with GetService<T>():

Service What it is Covered in
IGame The game itself The game loop
IGameWindow The window Windows and display
IDisplayController, IRenderSurface VSync, fullscreen and the logical size Windows and display
DisplayManager The displays and their modes Windows and display
ISystemThemeService The desktop's light or dark setting Windows and display
ILaunchUrls Links the game was started or handed Windows and display
IGameThread Work posted from other threads, run on the game's thread The game loop
IGraphics2D, ITextRenderer Drawing and text Drawing
IOffscreenRenderer, IRenderTargetAllocator Render targets Drawing
IRenderContextFactory Render encoders and render passes Render passes
IContentManager Textures and fonts Content
ShaderManager Materials and shader programs Materials, effects and lighting
IFullScreenEffectFactory Full-screen effects Materials, effects and lighting
ILightingManager Ambient light and lights Materials, effects and lighting
IAudioService Sound and music Audio
IInputBindingService, IKeyboardManager, IMouseManager Input Input
IEventAggregator Events between parts of the game Events

Unless the game registers an ILoggerFactory of its own, the engine registers a null one, and nothing is logged.

Registering implementations automatically

Some services are registered for you. A class that implements IRunOnInitialization or IAttachOnGameStart is registered as a singleton and run at startup, as described in The game loop.

The engine's registration helpers are public, and a game can use them to register every class that implements an interface:

public interface IRule
{
  bool Applies(string name);
}

public class LowerCaseRule : IRule
{
  public bool Applies(string name) => name == name.ToLowerInvariant();
}

protected override void Configure(IServiceCollection services)
{
  services.RegisterImplementationsOf<IRule>();
}

RegisterImplementationsOf<TAbstract>(lifetime) registers each non-abstract class that implements TAbstract as itself and as TAbstract. A class that is already registered as itself is not registered again. The default lifetime is transient.

FindAndRegisterImplementations<TAbstract>(lifetime) uses Scrutor to register each class that implements TAbstract as each interface it implements. It also defaults to transient.

Both search only the assemblies already loaded when they are called, and skip assemblies whose names begin with System or Microsoft. An assembly that is not yet loaded is not searched.