One of the features of Abathur is that modules can be added and removed without recompiling the agent. The example bot SC2Abathur configure modules at runtime based on a locally stored gamesettings file in JSON.

The only requirement is that the class implements the IModule interface;

public interface IModule {
  void Initialize();
  void OnStart();
  void OnStep();
  void OnGameEnded();
  void OnRestart();
}
The IModule interface from Abathur

An, somewhat boring, example of an implementation could look like this;

public class UserCreatedModule : IModule {
  void IModule.Initialize()
    => Console.WriteLine("Initalize | UserCreatedModule");
  void IModule.OnStart()
    => Console.WriteLine("Start | UserCreatedModule");
  void IModule.OnStep()
    => Console.WriteLine("Step | UserCreatedModule");
  void IModule.OnGameEnded()
    => Console.WriteLine("Game Ended | UserCreatedModule");
  void IModule.OnRestart()
    => Console.WriteLine("Restart | UserCreatedModule");
}

Normal Dependency Injection

Dependency Injection in NET Core usually looks something like this;

public Abathur(IModule module) {
  ...
}

IServiceProvider ConfigureServices() {
  ServiceCollection services = new ServiceCollection();
  services.AddScoped<IModule, UserCreatedModule>();
  services.AddSingleton<IAbathur, Abathur>();
  return services.BuildServiceProvider();
}

A service provider is created and all dependencies are added. The IServiceProvider then wires up all the dependencies behind the scenes.

But! Abathur often runs with multiple implementations of IModule at the same time – as it is recommended to break your agent into small independent modules. ServiceCollectionServiceExtensions is used to deal with this. It allows the service collection to contain multiple implementation of IModule and Abathur can then get them all injected as an IEnumerable instead.

public Abathur(IEnumerable<Module> module) {
  ...
}

IServiceProvider ConfigureServices() {
  ServiceCollection services = new ServiceCollection();
  ServiceCollectionServiceExtensions
  	.AddSingleton(services, typeof(IModule), typeof(UserCreatedModule));
  ServiceCollectionServiceExtensions
  	.AddSingleton(services, typeof(IModule), typeof(IntegratedModule));
  services.AddSingleton<IAbathur, Abathur>();
  return services.BuildServiceProvider();
}

... except. Abathur can not be pre-configured with user-created modules as they, at the time of writing, has not yet been implemented.


Dependency Injection with Reflection

Abathur can not predict the class name of your implementation, but it can cheat and look it up in the assembly. The method below search through all types in the assembly of IModule (Abathur) and the provided assembly (SC2Abathur, or your bot) and find the ones that should be added to DI.

private IEnumerable<Type> GetIModuleTypes(Assembly assembly) {
  TypeInfo info = typeof(IModule).GetTypeInfo();
  return
    // Find all types in Abathur assembly
    info.Assembly.GetTypes()
    // Add all types in SC2Abathur (users assembly)
    .Concat(assembly.GetTypes())
    // Filter to types that are assignable (inherits/implements) from IModule
    .Where(x => info.IsAssignableFrom(x))
    // Ensure it is a non-abstract class
    .Where(x => x.IsClass && !x.IsAbstract && x.IsPublic);
}

All the available types in the two assemblies are then filtered through to find types that are assignable to IModule – which it is if;

  • x and IModule is the same type.
  • x inherits from IModule either directly or indirectly.
  • x is an interface that implements IModule.

It is then assured that only non-abstract, public, classes are considered. Before we start utilizing the code snippet, lets make it generic;

IEnumerable<Type> GetTypes<T>(Assembly assembly) {
  TypeInfo info = typeof(T).GetTypeInfo();
  return
    info.Assembly.GetTypes()
    .Concat(assembly.GetTypes())
    .Where(x => info.IsAssignableFrom(x))
    .Where(x => x.IsClass && !x.IsAbstract && x.IsPublic);
}

Dependency Injection in Abathur

Abathur utilize the above method to find all implementation of IModule and then excludes anything not mentioned in the gamesettings file. This allows users of the framework to simply inherit from the interface and not give it a second thought – quick and easy... and a bit dirty.

It also includes all implementations of IReplaceableModule into the ServiceCollection. This allows other modules to access these through dependency injection. An example of this can be seen in RandomDemo; a small module that injects modules depending on which race the player is at startup.

IServiceProvider ConfigureServices(Assembly assembly, string[] classnames)
{
  var modules = GetTypes<IModule>(assembly)
    .Where(x => classnames.Contains(x.Name));
    
  ServiceCollection services = new ServiceCollection();
  foreach (Type type in GetTypes<IReplaceableModule>(assembly))
    services.AddScoped(type, type);
  foreach (var type in modules)
      ServiceCollectionServiceExtensions
      .AddSingleton(services, typeof(IModule), type);
  services.AddSingleton<IAbathur, Abathur>();
  return services.BuildServiceProvider();
}

Conclusion

This post showcase how Abathur use reflection to ease dependency injection for users. Is this a good way to handle dependency injection in your project? Probably not.

The only use case I can come up with is framework-style applications – like Abathur – where the classes you must include in the service collection is unknown at compilation time (frameworks compilation time, not project).

But even then! Don't do it. If you are creating a publicly available framework that requires this kind of dependency injection consider exposing it to the user instead of introducing this kind of 'magic'. Let the users configure the service.

Dependency Injection is a common tool that all NET Core developers are familiar with and probably already have setup in the project they are using your framework for anyways.

But why did you then ...

I was so preoccupied with whether or not I could,
I didn’t stop to think if I should.


It does provide a tiny benefit. Creating a simple SC2 Agent is an ideal hobby project for people interesting in programming – and I would love to make it more accessible through Abathur. Introducing this piece of magic code hides away some of the complexity. My hope is it can ease entry for people who are new at programming or simply join the NET Core team from other languages.

Hello Marine
A tutorial for writing your very first primitive agent using the NET Core StarCraft II AI Framework ‘Abathur’. In this tutorial we will setup Abathur, built a Marine and attack the enemy base!
New to StarCraft II Agents? Start here.