Table of Contents

How it finds your code

When the host starts, it looks through your assemblies for classes it knows and registers each one with dependency injection. You don't keep a list of services or packet handlers: write the class, and the host finds it.

What is scanned

The host looks at:

  • the entry assembly, the one with Main;
  • every assembly that one references, and the assemblies those reference, found by name;
  • every .dll and .exe in the executable's folder, which is how plugins are added.

The framework's own assemblies (System.* and Microsoft.*) are not scanned, and neither are abstract classes or interfaces.

An assembly that holds services but has no type your code uses can lose its reference when the game is compiled, and a single-file publish then doesn't find it. Use one of its types from your code, or put it in the executable's folder.

Leaving a type out

Mark a class, or a whole assembly, with DoNotRegisterAttribute. The attribute on an assembly leaves out all of its types, but the assemblies it references are still scanned.

using BSGameLib.Core.Attributes;
using BSGameLib.Networking;

public class PingPacket : IDataPacket
{
  public long SentAt { get; set; }
}

// Not registered: the host finds no handler for PingPacket here.
[DoNotRegister]
public class DebugPingHandler : IDataPacketHandler<PingPacket>
{
  public void Handle(INetConnection connection, PingPacket packet) { }
}

What is registered

What you write What the host does
A class implementing IGameServer A singleton. There must be exactly one, or the host fails to start.
A class implementing IInitializer A singleton, initialized before the server listens.
A class implementing IDataPacket Registered as a packet type, so it can be received. A packet with no handler is accepted and does nothing.
A class implementing IDataPacketHandler<TPacket> A singleton, which handles that one packet type.
A class implementing IServiceBootstrapper Its Configure runs once, while the host is built. It needs a public parameterless constructor, and it is not kept.
A class implementing IInjectedDependency, or an interface that derives from it One shared singleton, registered under the class and under each of those interfaces.
A class implementing IRepository<TKey, TEntity> or IRepositoryService<TEntity> A shared singleton. See Repositories.
A class implementing ICommandProvider Registered. Its command types are loaded into the chat engines before the server listens. See Chat on a server.
A class implementing ICommandFactory<TCommand> A singleton, used to create that command each time it is typed.
A class deriving from BaseChatEngine<TSource> The server uses it for its source type, in place of the default chat engine, and initializes it before listening. See Chat on a server.
ILogger and ILogger<T> Available to inject. See Logging.
IHttpClientFactory Available to inject. Create clients from it, rather than making HttpClients yourself.

The engine provides these services too: IServerSettings (the settings you passed to the host), IListenSocket, IServerAdapter, IChatProvider and IChatEngine<TSource>. Session services come from AddSessionManagement, described in Sessions and connections.

A class can't implement IDataPacketHandler<T> for two packet types: the compiler rejects it.

Packet type names must be unique. Two packet classes with the same name (in any scanned assembly) stop the host at startup, naming both, because a packet is identified on the wire by its type name.

Two classes for one service

Scanned registrations don't replace one another. When two classes implement the same interface, the first one found is used, and the order is not something to rely on. To choose, register the class yourself in Configure; a registration made there is the last one, so it wins.

using BSGameLib.Server;
using Microsoft.Extensions.DependencyInjection;

public interface IWorldClock : IInjectedDependency
{
  long Now { get; }
}

public class WorldClock : IWorldClock
{
  public long Now => Environment.TickCount64;
}

public class FixedClock : IWorldClock
{
  public long Now => 0;
}

public class TestServerHost(IServerSettings settings) : ServerHost(settings)
{
  protected override void Configure(IServiceCollection services) =>
    services.AddSingleton<IWorldClock, FixedClock>();
}

Lifetimes and injection

Everything the host registers is a singleton, except loggers, which are transient, and chat commands, which get a new instance for each time they are typed. Constructor injection works for every class the host creates: the game server, handlers, services and commands.

A packet handler is one object for every connection and every packet, so it must not keep one player's state in a field. Keep per-player state in a service, keyed by session (see Sessions and connections).

The packet handler in this example takes the clock from dependency injection:

using BSGameLib.Networking;
using BSGameLib.Server;

public interface IWorldClock : IInjectedDependency
{
  long Now { get; }
}

public class WorldClock : IWorldClock
{
  public long Now => Environment.TickCount64;
}

public class PingPacket : IDataPacket
{
  public long SentAt { get; set; }
}

// Found and registered automatically. The clock is injected.
public class PingHandler(IWorldClock clock) : IDataPacketHandler<PingPacket>
{
  public void Handle(INetConnection connection, PingPacket packet) =>
    Console.WriteLine($"Ping from {connection.IpAddress}: {clock.Now - packet.SentAt} ms");
}