<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Paul Stamp</title>
    <description>The latest articles on DEV Community by Paul Stamp (@paulnonatomic).</description>
    <link>https://dev.to/paulnonatomic</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1042404%2Fecfbcd46-99c3-4b2a-b5f5-f43f8ccf2520.jpg</url>
      <title>DEV Community: Paul Stamp</title>
      <link>https://dev.to/paulnonatomic</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/paulnonatomic"/>
    <language>en</language>
    <item>
      <title>ServiceKit V2 — The Async Service Locator for Unity</title>
      <dc:creator>Paul Stamp</dc:creator>
      <pubDate>Sat, 18 Apr 2026 10:21:06 +0000</pubDate>
      <link>https://dev.to/paulnonatomic/servicekit-v2-the-async-service-locator-for-unity-4840</link>
      <guid>https://dev.to/paulnonatomic/servicekit-v2-the-async-service-locator-for-unity-4840</guid>
      <description>&lt;p&gt;I just shipped V2 of &lt;a href="https://github.com/PaulNonatomic/ServiceKit" rel="noopener noreferrer"&gt;ServiceKit&lt;/a&gt;, 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a service locator, not a "proper" DI framework
&lt;/h2&gt;

&lt;p&gt;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 &lt;em&gt;something&lt;/em&gt; 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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;The two classic critiques of service locators are hidden dependencies and async timing. V2 is where I've tried to put both to bed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hidden dependencies, un-hidden
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;[InjectService]&lt;/code&gt; 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.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Service&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IPlayerController&lt;/span&gt;&lt;span class="p"&gt;))]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PlayerController&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ServiceKitBehaviour&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;IPlayerController&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;InjectService&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;IPlayerService&lt;/span&gt; &lt;span class="n"&gt;_playerService&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;InjectService&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;IAudioService&lt;/span&gt;  &lt;span class="n"&gt;_audioService&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;InitializeService&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_playerService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LoadPlayer&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two compile-time analyzers ship in V2:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SK003&lt;/strong&gt; flags &lt;code&gt;[Service(typeof(IFoo))]&lt;/code&gt; attributes on classes that don't actually implement &lt;code&gt;IFoo&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SK005&lt;/strong&gt; catches &lt;code&gt;ServiceKitBehaviour&lt;/code&gt; subclasses that forget &lt;code&gt;base.Awake()&lt;/code&gt;.
Both catch the kind of bug that's annoying to hunt down at runtime.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Async resolution, the real headache fixed
&lt;/h2&gt;

&lt;p&gt;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 &lt;code&gt;IAudioService&lt;/code&gt; in &lt;code&gt;Awake&lt;/code&gt; on the wrong GameObject, it isn't registered yet, you get null, and you're debugging initialisation order for three hours.&lt;/p&gt;

&lt;p&gt;V2 resolves this cleanly. Field injection is a single await:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// V1&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_serviceKit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Inject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithErrorHandling&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithTimeout&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ExecuteWithCancellationAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// V2&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_serviceKit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;InjectAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;destroyCancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And under the hood there's a new atomic 3-state resolution primitive that performs registration and readiness checks inside a single lock:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryResolveService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IMyService&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// ServiceResolutionStatus.Ready&lt;/span&gt;
&lt;span class="c1"&gt;// ServiceResolutionStatus.RegisteredNotReady&lt;/span&gt;
&lt;span class="c1"&gt;// ServiceResolutionStatus.NotRegistered&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Previously, consumers had to check registration, then readiness, then fetch, giving the world three chances to change between steps. V2 closes that window.&lt;/p&gt;

&lt;p&gt;Alongside that: task forwarding in &lt;code&gt;GetServiceAsync&lt;/code&gt; now happens inside locks, double-registration is blocked with &lt;code&gt;Interlocked&lt;/code&gt; operations, and circular-dependency detection uses types rather than string comparison. Less likely to bite you in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Generics out, attributes in
&lt;/h2&gt;

&lt;p&gt;The other big ergonomic change. V1 asked you to inherit from &lt;code&gt;ServiceKitBehaviour&amp;lt;T&amp;gt;&lt;/code&gt;, which got painful fast when your own classes needed generics too. V2 replaces that with a plain base class and an explicit attribute:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// V1&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AudioManager&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ServiceKitBehaviour&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IAudioService&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;,&lt;/span&gt; &lt;span class="n"&gt;IAudioService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// V2&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Service&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IAudioService&lt;/span&gt;&lt;span class="p"&gt;))]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AudioManager&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ServiceKitBehaviour&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;IAudioService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Registration intent is declarative instead of tangled in type parameters, and abstract base classes can keep their own generics without fighting ServiceKit's.&lt;/p&gt;

&lt;h2&gt;
  
  
  Migrating from V1
&lt;/h2&gt;

&lt;p&gt;Mostly mechanical: drop the generic parameter on &lt;code&gt;ServiceKitBehaviour&lt;/code&gt;, add &lt;code&gt;[Service(typeof(IYourInterface))]&lt;/code&gt;, and replace builder chains with &lt;code&gt;InjectAsync(this, token)&lt;/code&gt;. The README has the full migration guide, including how to handle abstract base classes that mix ServiceKit generics with their own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Install via Package Manager → &lt;strong&gt;Add package from git URL&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://www.pkglnk.dev/servicekit.git
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Repo and docs: &lt;a href="https://github.com/PaulNonatomic/ServiceKit" rel="noopener noreferrer"&gt;github.com/PaulNonatomic/ServiceKit&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>gamedev</category>
      <category>opensource</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
