I just shipped V2 of ServiceKit, my lightweight dependency management package for Unity. Before I get into what's new, I want to address the thing some of you are already typing into the comments: yes, ServiceKit is a service locator. On purpose. I think that's the right shape for Unity, and V2 is where I've stopped hedging about it.
Why a service locator, not a "proper" DI framework
The "service locator is an anti-pattern" mantra largely comes from enterprise .NET, where you control object construction, processes are long-lived, and writing an installer for two hundred services is routine. Unity isn't that world. Unity instantiates your components for you, so something has to do late-binding resolution regardless of what you call it. The heavyweight DI frameworks respond to that by adding more ceremony, not less. Contexts, scopes, installers, factories, binding chains. Adopting Zenject is a lifestyle decision. You don't use Zenject so much as restructure your project around it.
Service locators are the opposite trade. Low friction, no framework imposed on the rest of your codebase, incremental. You can drop ServiceKit in to solve one pain point without rewriting everything around it.
The two classic critiques of service locators are hidden dependencies and async timing. V2 is where I've tried to put both to bed.
Hidden dependencies, un-hidden
[InjectService] on fields surfaces the dependency graph at the class level. It's not quite a constructor signature, but it's visible to code review, tooling, and the new Roslyn analyzers. That covers most of the "you can't see what a class needs" complaint.
[Service(typeof(IPlayerController))]
public class PlayerController : ServiceKitBehaviour, IPlayerController
{
[InjectService] private IPlayerService _playerService;
[InjectService] private IAudioService _audioService;
protected override void InitializeService()
{
_playerService.LoadPlayer();
}
}
Two compile-time analyzers ship in V2:
-
SK003 flags
[Service(typeof(IFoo))]attributes on classes that don't actually implementIFoo. -
SK005 catches
ServiceKitBehavioursubclasses that forgetbase.Awake(). Both catch the kind of bug that's annoying to hunt down at runtime.
Async resolution, the real headache fixed
This is the one I'm most pleased with. The traditional service locator's real failure mode in Unity isn't philosophical, it's the timing problem. You ask for IAudioService in Awake on the wrong GameObject, it isn't registered yet, you get null, and you're debugging initialisation order for three hours.
V2 resolves this cleanly. Field injection is a single await:
// V1
await _serviceKit.Inject(this)
.WithErrorHandling()
.WithTimeout()
.ExecuteWithCancellationAsync(token);
// V2
await _serviceKit.InjectAsync(this, destroyCancellationToken);
And under the hood there's a new atomic 3-state resolution primitive that performs registration and readiness checks inside a single lock:
var status = locator.TryResolveService(typeof(IMyService), out var service);
// ServiceResolutionStatus.Ready
// ServiceResolutionStatus.RegisteredNotReady
// ServiceResolutionStatus.NotRegistered
Previously, consumers had to check registration, then readiness, then fetch, giving the world three chances to change between steps. V2 closes that window.
Alongside that: task forwarding in GetServiceAsync now happens inside locks, double-registration is blocked with Interlocked operations, and circular-dependency detection uses types rather than string comparison. Less likely to bite you in production.
Generics out, attributes in
The other big ergonomic change. V1 asked you to inherit from ServiceKitBehaviour<T>, which got painful fast when your own classes needed generics too. V2 replaces that with a plain base class and an explicit attribute:
// V1
public class AudioManager : ServiceKitBehaviour<IAudioService>, IAudioService { }
// V2
[Service(typeof(IAudioService))]
public class AudioManager : ServiceKitBehaviour, IAudioService { }
Registration intent is declarative instead of tangled in type parameters, and abstract base classes can keep their own generics without fighting ServiceKit's.
Migrating from V1
Mostly mechanical: drop the generic parameter on ServiceKitBehaviour, add [Service(typeof(IYourInterface))], and replace builder chains with InjectAsync(this, token). The README has the full migration guide, including how to handle abstract base classes that mix ServiceKit generics with their own.
Try it
Install via Package Manager → Add package from git URL:
https://www.pkglnk.dev/servicekit.git
Repo and docs: github.com/PaulNonatomic/ServiceKit
MIT licensed. Issues and PRs welcome. If you think I'm wrong about the locator vs DI trade-off, I want to hear it. V2 is the foundation I want to build on, so I'm happy to argue about it.
Top comments (1)
The async service locator pattern makes a lot of sense for Unity where initialization order can be unpredictable. V2 making it explicit rather than hedging is the right call!