How Service Registration Works
OneLastClick.Services uses Roslyn source generators to remove most of the boilerplate normally required to register and inject services.
Instead of maintaining a central list of services, you annotate your classes with service attributes:
[ApplicationService]public class PlayerService{}
[MonoBehaviourService(typeof(IPlayerView))]public partial class PlayerView : MonoBehaviour{ [InjectService] private PlayerService _playerService;}At compile time, the source generators inspect the project and generate the registration and injection code automatically.
The overall flow looks like this:
COMPILE TIME │ ▼ ┌─────────────────────┐ │ Roslyn Source │ │ Generators │ └──────────┬──────────┘ │ ┌──────────┴──────────┐ │ │ ▼ ▼ Application Services MonoBehaviour Services │ │ │ ▼ │ RegisterServices(...) │ │ ▼ │ GeneratedApplicationServices │ │ │ └──────────┬──────────┘ │ │ RUNTIME │ ▼ ApplicationServices │ ▼ ServicesCollectionBuilder │ ▼ ServiceCollection │ ▼ StartServices() │ ▼ InjectServices(...)The three source generators
Section titled “The three source generators”There are three generators responsible for the system:
| Generator | Purpose |
|---|---|
PerAssemblyApplicationServiceRegisterSourceGenerator |
Finds [ApplicationService] classes and generates their registrations |
MonoBehaviourServiceSourceGenerator |
Generates registration code for [MonoBehaviourService] classes |
InjectServicesSourceGenerator |
Generates InjectServices() implementations for [InjectService] members |
The important distinction is that registration and injection are generated separately.
1. Application services
Section titled “1. Application services”An application service is a service that exists independently of a particular MonoBehaviour.
For example:
[ApplicationService]public class PlayerService{ public void LoadPlayer() { }}The PerAssemblyApplicationServiceRegisterSourceGenerator searches the compilation for types decorated with the application’s service attribute:
If no application services are found, the generator has nothing to generate.
Otherwise, it sorts the services deterministically and generates a class similar to:
public static partial class GeneratedApplicationServices{ [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] private static void RegisterApplicationServices() { ApplicationServices.AddApplicationServicesBuilderFunction(RegisterGeneratedApplicationServices); }
public static void RegisterGeneratedApplicationServices(ServicesCollectionBuilder builder) { builder.Register<PlayerService>(); }}Why is this generated per assembly?
Section titled “Why is this generated per assembly?”Each assembly can contain its own application services.
The generator runs against each compilation and discovers the services belonging to that compilation. The generated registration function is then added to the central ApplicationServices registry.
This means you don’t need to maintain something like:
services.Register<PlayerService>();services.Register<InventoryService>();services.Register<AudioService>();services.Register<SaveGameService>();in one manually maintained location.
Adding:
[ApplicationService]public class InventoryService{}is enough for the generator to discover it.
2. Registering the generated functions
Section titled “2. Registering the generated functions”The generated application-service class uses Unity’s subsystem initialization:
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]private static void RegisterApplicationServices(){ ApplicationServices.AddApplicationServicesBuilderFunction(RegisterGeneratedApplicationServices);}This happens before the actual service collection is started.
The generated function is stored by ApplicationServices:
private static readonly List<Action<ServicesCollectionBuilder>> RegisteredBuilderFunctions = new();
public static void AddApplicationServicesBuilderFunction(Action<ServicesCollectionBuilder> registration){ Debug.Assert(registration != null, "provided function is null.");
RegisteredBuilderFunctions.Add(registration);}At this point, the services haven’t been constructed yet.
The system has only collected functions that know how to build the service collection.
3. Starting application services
Section titled “3. Starting application services”The actual service collection is created when:
ApplicationServices.StartFromGameObject(gameObject);is called.
The startup process is:
StartFromGameObject() │ ▼new ServicesCollectionBuilder() │ ▼Execute generated registration functions │ ▼Register the GameObject │ ▼builder.Build() │ ▼ServiceCollection │ ▼StartServices()The important part of StartFromGameObject() is:
ServicesCollectionBuilder builder = new();
foreach (var generatedRegistrationBuilderFunctionin RegisteredBuilderFunctions){ try { generatedRegistrationBuilderFunction?.Invoke(builder); } catch (Exception e) { Debug.LogException(e); }}
builder.RegisterGameObject(gameObject);
_serviceCollection = builder.Build();
StartServices();Every generated registration function gets the same builder.
For example, imagine two assemblies generated:
RegisterAssemblyA(builder);RegisterAssemblyB(builder);The resulting builder receives registrations from both assemblies.
MonoBehaviour services
Section titled “MonoBehaviour services”MonoBehaviour services are slightly different.
A MonoBehaviour is already a Unity object, so the service system needs to register the existing instance, rather than create a new instance using the normal service registration mechanism.
For example:
[MonoBehaviourService]public partial class PlayerController : MonoBehaviour{}The MonoBehaviourServiceSourceGenerator discovers this type and generates a partial implementation.
Conceptually, it generates:
public partial class PlayerController : IServiceRegisterer{ public void RegisterServices(ServicesCollectionBuilder servicesCollectionBuilder) { servicesCollectionBuilder.Register<PlayerController>(this); }}The important detail is:
Register<PlayerController>(this);The this reference is the actual MonoBehaviour instance.
Registering interfaces
Section titled “Registering interfaces”A MonoBehaviourService can specify types that it should be registered as.
For example:
[MonoBehaviourService(typeof(IPlayerView))]public partial class PlayerView : MonoBehaviour{}The generator reads the attribute arguments and then generates:
public void RegisterServices(ServicesCollectionBuilder servicesCollectionBuilder){ servicesCollectionBuilder.Register<IPlayerView, PlayerView>();}This allows consumers to depend on the abstraction:
[InjectService]private IPlayerView _playerView;rather than the concrete PlayerView.
Multiple registration types are also possible:
[MonoBehaviourService(typeof(IPlayerView), typeof(ISelectable))]public partial class PlayerView : MonoBehaviour{}which results conceptually in:
servicesCollectionBuilder.Register<IPlayerView, PlayerView>();
servicesCollectionBuilder.Register<ISelectable, PlayerView>();Service injection
Section titled “Service injection”Once services can be registered, the final piece is injection.
A class can mark fields or properties with [InjectService]:
public partial class PlayerController : MonoBehaviour{ [InjectService] private PlayerService _playerService;
[InjectService] public IPlayerView PlayerView { get; set; }}The InjectServicesSourceGenerator finds types containing injectable members. For each type, it generates an InjectServices() method.
The generated implementation is conceptually:
public partial class PlayerController : IServiceInjectable{ public virtual void InjectServices(IServiceResolver resolver) { _playerService = resolver.Get<PlayerService>(); PlayerView = resolver.Get<IPlayerView>(); }}There is no reflection involved in the generated assignments.
The generated code directly calls:
resolver.Get<T>();for each injectable member.
Why partial classes are used
Section titled “Why partial classes are used”The generators don’t replace your class.
Instead, they add another part of it.
Your source:
public partial class PlayerController : MonoBehaviour{ [InjectService] private PlayerService _playerService;}and generated source:
public partial class PlayerController{ public void InjectServices(IServiceResolver resolver) { _playerService = resolver.Get<PlayerService>(); }}become one class after compilation.
This keeps generated implementation details out of the handwritten source.
Inheritance and injection
Section titled “Inheritance and injection”Injection also takes inheritance into account.
Suppose a base class contains an injectable member:
public partial class BaseController : MonoBehaviour{ [InjectService] protected PlayerService PlayerService;}and a derived class contains another:
public partial class PlayerController : BaseController{ [InjectService] private IPlayerView _playerView;}The generator detects that the base type is also injectable.
Instead of replacing the base implementation, it generates an override:
public override void InjectServices( IServiceResolver resolver){ base.InjectServices(resolver);
_playerView = resolver.Get<IPlayerView>();}This creates an injection chain through the inheritance hierarchy.
Conceptually:
PlayerController.InjectServices() │ ▼BaseController.InjectServices() │ ├── PlayerService │ ▼PlayerController │ └── IPlayerViewThis is why the generator checks:
HasInjectableBaseType(...)before deciding whether to implement or override InjectServices().
The complete startup flow
Section titled “The complete startup flow”Putting everything together, the lifecycle looks like this:
Compile time
Section titled “Compile time”C# source │ ├── [ApplicationService] │ ├── [MonoBehaviourService] │ └── [InjectService] │ ▼ Roslyn generators │ ├──────────────────────────┐ │ │ ▼ ▼GeneratedApplicationServices Generated partial classes │ ├── RegisterServices() │ └── InjectServices()Unity initialization
Section titled “Unity initialization”For application services, Unity invokes:
RegisterApplicationServices();The generated method registers its builder function:
ApplicationServices.AddApplicationServicesBuilderFunction(RegisterGeneratedApplicationServices);At this stage, the system knows how to register the services.
Application startup
Section titled “Application startup”Eventually:
ApplicationServices.StartFromGameObject(gameObject);creates the builder.
Every generated registration function is invoked:
foreach (var registration in RegisteredBuilderFunctions){ registration(builder);}The builder now contains registrations for:
- application services
MonoBehaviourservices- other registrations associated with the startup GameObject
The builder is then converted into the runtime collection:
_serviceCollection = builder.Build();Starting services
Section titled “Starting services”After building:
StartServices();is called.
This performs the actual service startup:
_serviceCollection.StartServices();The service collection is now considered started.
private static bool IsStarted =>_serviceCollection != null &&_serviceCollection.IsStarted;Getting a service
Section titled “Getting a service”Once the collection has started, services can be retrieved through:
ApplicationServices.Get<PlayerService>();or safely:
if (ApplicationServices.TryGet<PlayerService>(out PlayerService playerService)){ // Use playerService}The static API delegates to the current ServiceCollection:
public static TI Get<TI>(){ return _serviceCollection.Get<TI>();}and:
public static bool TryGet<TI>(out TI service){ if (HasStarted() == false) { service = default; return false; }
return _serviceCollection.TryGet<TI>(out service);}The generated injection code uses the same underlying resolver mechanism.
Update lifecycle
Section titled “Update lifecycle”After startup, ApplicationServices.TryUpdate() can be called.
If the service collection has not started, nothing happens:
if (IsStarted == false){ return false;}Otherwise:
UpdateServices();return true;updates both application services and scene services:
private static void UpdateServices(){ Debug.Assert(IsStarted == true, "Services not started");
_serviceCollection.UpdateServices(); SceneServices.UpdateServices();}The runtime lifecycle is therefore:
Build │ ▼StartServices() │ ▼UpdateServices() │ ▼UpdateServices() │ ▼UpdateServices() │ ▼Application quitting │ ▼StopServices()Shutdown
Section titled “Shutdown”ApplicationServices subscribes to Unity’s quitting event when services start:
Application.quitting += Stop;When Unity quits, Stop() is called.
It stops the services:
StopServices();and clears the service collection:
_serviceCollection = null;StopServices() also clears the generated registration functions:
RegisteredBuilderFunctions.Clear();The complete shutdown sequence is:
Application.quitting │ ▼ Stop() │ ▼ StopServices() │ ├── SceneServices.StopSceneServices() │ └── ServiceCollection.StopServices() │ ▼_serviceCollection = nullWhy source generation is used
Section titled “Why source generation is used”The service system could theoretically perform discovery using reflection at runtime.
Instead, discovery happens at compile time.
For example, rather than runtime code searching for:
[ApplicationService]the generator turns:
[ApplicationService]public class PlayerService{}into an explicit registration:
builder.Register<PlayerService>();Likewise:
[InjectService]private PlayerService _playerService;becomes:
_playerService = resolver.Get<PlayerService>();This has several useful properties:
- No runtime type scanning is required for these operations.
- No reflection is required to perform the generated injection assignments.
- The compiler knows the concrete types involved.
- Adding a service does not require editing a central registration file.
- Generated registrations are deterministic.
- The generated code can be inspected when debugging compilation.
Deterministic registration order
Section titled “Deterministic registration order”Application services are explicitly sorted before their registrations are generated:
DeterministicallySortServiceTypes(applicationServices);The sort key is:
type.ContainingAssembly.Name + "." + type.ToDisplayString( SymbolDisplayFormat.FullyQualifiedFormat);This means the generated registration order does not depend on arbitrary discovery order.
Conceptually:
AssemblyA.PlayerServiceAssemblyA.SaveServiceAssemblyB.AudioServiceAssemblyB.InventoryServicewill always be emitted in the same order for the same set of source types.
This makes generated output more predictable and avoids unnecessary changes caused purely by discovery ordering.
What you need to write
Section titled “What you need to write”For most services, the handwritten code is intentionally small.
Application service
Section titled “Application service”[ApplicationService]public class InventoryService{}MonoBehaviour service
Section titled “MonoBehaviour service”[MonoBehaviourService]public partial class PlayerController : MonoBehaviour{}MonoBehaviour registered as an interface
Section titled “MonoBehaviour registered as an interface”[MonoBehaviourService(typeof(IPlayerView))]public partial class PlayerView : MonoBehaviour{}Injecting a service
Section titled “Injecting a service”public partial class PlayerController : MonoBehaviour{ [InjectService] private InventoryService _inventoryService;}The generators take care of the rest.
The mental model
Section titled “The mental model”The easiest way to think about the system is to separate it into three stages:
1. DISCOVER │ │ Source generators inspect attributes ▼2. GENERATE │ │ Registration + injection code is compiled ▼3. RUN │ │ ApplicationServices builds and starts the collection ▼ SERVICESOr, more concretely:
[ApplicationService] │ ▼builder.Register<T>() │ ▼ServiceCollection │ ▼StartServices() │ ▼resolver.Get<T>() │ ▼[InjectService] memberThe important idea is that attributes describe intent, source generators turn that intent into normal C# code, and ApplicationServices coordinates that generated code at runtime.