Skip to content

Commit c9f16b8

Browse files
authored
Merge pull request #97 from KuraiAndras/feature/overhaul-unity
Overhaul unity
2 parents 81ebaf9 + 75e1948 commit c9f16b8

File tree

116 files changed

+950
-1651
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

116 files changed

+950
-1651
lines changed

.nuke/build.schema.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"AppVeyor",
3030
"AzurePipelines",
3131
"Bamboo",
32+
"Bitbucket",
3233
"Bitrise",
3334
"GitHubActions",
3435
"GitLab",

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
# 9.0.0
2+
- Remove `Injecter.Hosting.Unity`
3+
- Rework `Injecter.Unity` now it uses `MonoInjector` and `MonoDisposer`
4+
- `IScopeStore` now accepts objects instead of generics
5+
- `Injecter` now returns null if the target type has no injectable members
6+
- `Injecter` injects faster and with less allocations
7+
- `IInjecter`'s generic overload is removed
8+
- New `AppInstaller` templates
9+
110
# 8.0.1
211
- Move AppInstallerTemplateItem to editor folder
312

Documentation/documentation/injecter.unity.md

Lines changed: 128 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -5,114 +5,165 @@ Since version 3.0.1 you need to provide the following dlls yourself:
55
- Microsoft.Extensions.DependencyInjection
66
- Microsoft.Extensions.DependencyInjection.Abstractions
77

8-
## Initialize
9-
Create a class that inherits from InjectStarter:
10-
```c#
11-
// Customize script execution order, so Awake is called first in you scene
12-
// Usually -999 works nicely
13-
[DefaultExecutionOrder(-999)]
14-
public sealed class ExampleInjector : InjectStarter
15-
{
16-
// Override CreateServiceProvider to add service registrations
17-
protected override IServiceProvider CreateServiceProvider()
18-
{
19-
IServiceCollection services = new ServiceCollection();
8+
> [!NOTE]
9+
> The recommended way of installing NuGet packages is through the [UnityNuget](https://github.com/xoofx/UnityNuGet) project
2010
21-
// Mandatory to call AddSceneInjector, optionally configure options
22-
services.AddSceneInjector(
23-
injecterOptions => injecterOptions.UseCaching = true,
24-
sceneInjectorOptions =>
25-
{
26-
sceneInjectorOptions.DontDestroyOnLoad = true;
27-
sceneInjectorOptions.InjectionBehavior = SceneInjectorOptions.Behavior.Factory;
28-
});
11+
## Fundamentals
2912

13+
The `Injecter.Unity` lets you set up the following flow:
3014

31-
// Use the usual IServiceCollection methods
32-
services.AddTransient<IExampleService, ExampleService>();
15+
- A "composition root" is initialized part of the entry point of the application
16+
- Create a script which needs to be injected
17+
- Add `MonoInjector` to the `GameObject` hosting the script
18+
- `MonoInjector` runs at `Awake`, and it's execution order (`int.MinValue` - first) is run before your own component's `Awake`. Every injected script will have it's own `IServiceScope` derived from the root scope. This scope can be retrieved through the `IScopeStore`, and the owner of the scope is the script being injected
19+
- When the `GameObject` is destroyed, `MonoDisposer` will run during the `OnDestroy` method, with an execution order of `int.MaxValue` - last
3320

34-
// Resolve scripts already in the scene with FindObjectOfType()
35-
services.AddSingleton<MonoBehaviourService>(_ => GameObject.FindObjectOfType<MonoBehaviourService>());
21+
## Getting started
3622

37-
// Either:
23+
### Install dependencies
3824

39-
// Return a built ServiceProvider
40-
return services.BuildServiceProvider();
41-
}
42-
}
25+
1. Install `Injecter` and `Microsoft.Extensions.DependencyInjection` through [UnityNuget](https://github.com/xoofx/UnityNuGet#unitynuget-).
26+
2. Install `Injecter.Unity` through [openupm](https://openupm.com/packages/com.injecter.unity/)
27+
```bash
28+
openupm add com.injecter.unity
4329
```
4430

45-
Add this script to any one GameObject in your scene.
46-
47-
## Usage in MonoBehavior
31+
### Setup root
4832

49-
Use the InjectAttribute to inject into a MonoBehavior:
33+
Either create manually, or through the `Assets / Injecter` editor menu, create your composition root.
5034

51-
**When using the CompositionRoot Behavior option you need to inherit from InjectedMonoBehavior**
35+
```csharp
36+
#nullable enable
37+
using Injecter;
38+
using Microsoft.Extensions.DependencyInjection;
39+
using UnityEngine;
5240

53-
```c#
54-
public class ExampleScript : MonoBehaviour
41+
public static class AppInstaller
5542
{
56-
[Inject] private readonly IExampleService1 _exampleService1;
57-
[Inject] private IExampleService2 ExampleService2 { get; }
43+
/// <summary>
44+
/// Set this from test assembly to disable
45+
/// </summary>
46+
public static bool Run { get; set; } = true;
47+
48+
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSplashScreen)]
49+
public static void Install()
50+
{
51+
if (!Run) return;
52+
53+
var serviceProvider = new ServiceCollection()
54+
.Configure()
55+
.BuildServiceProvider(true);
56+
57+
// Injected scripts will get the root service provider from this instance
58+
CompositionRoot.ServiceProvider = serviceProvider;
5859

59-
private IExampleService3 _exampleService3;
60+
Application.quitting += OnQuitting;
6061

61-
[Inject]
62-
private void Construct(IExampleService3 exampleService3)
62+
/// <summary>
63+
/// Will dispose of all services when quitting
64+
/// </summary>
65+
async void OnQuitting()
66+
{
67+
Application.quitting -= OnQuitting;
68+
69+
await serviceProvider.DisposeAsync().ConfigureAwait(false);
70+
}
71+
}
72+
73+
public static IServiceCollection Configure(this IServiceCollection services)
6374
{
64-
_exampleService3 = exampleService3;
75+
services.AddInjecter(o => o.UseCaching = true);
76+
// TODO: Add services
77+
78+
return services;
6579
}
6680
}
6781
```
6882

69-
Supported injection methods for InjectAttribute: Field, Property, Method. Injection happens in this order. **Constructor injection does not work.**
83+
### Inject into `MonoBehaviours`
7084

71-
## Usage in Prefabs when using the Factory Behavior option
85+
Create a script which will receive injection
86+
87+
```csharp
88+
[RequireComponent(typeof(MonoInjector))]
89+
public class MyScript : MonoBehaviour
90+
{
91+
[Inject] private readonly IMyService _service = default!;
92+
}
93+
```
7294

73-
Injecting into prefabs:
95+
### Add `MonoInjector`
7496

75-
```c#
76-
// Get a prefab that contains a script which needs injection.
77-
GameObject prefab = ;
78-
// IGameObjectInjector and ISceneInjector are services added by default to Services
79-
IGameObjectFactory gameObjectFactory = ;
80-
ISceneInjector sceneInjector = ;
97+
If you decorate your script with `[RequireComponent(typeof(MonoInjector))]` then, when adding the script to a `GameObject` the editor will add the `MonoInjector` and the `MonoDisposer` script to your `GameObject`. If for some reason this does not happen (for example, when changing an already living script into one needing injection), either add the `MonoInjector` component manually, or use the editor tools included to add the missing components to scenes or prefabs (will search all instances) through the editor menu `Tools / Injecter / ...`
8198

82-
// Either:
99+
## Manual injection
83100

84-
// Instantiate the usual way
85-
var instance = GameObject.Instantiate(prefab);
86-
// Inject into freshly created GameObject
87-
sceneInjector.InjectIntoGameObject(instance);
101+
When dynamically adding an injectable script to a `GameObject` you need to handle the injection and disposal manually.
88102

89-
// Or:
103+
```csharp
90104

91-
// Use IGameObjectFactory which wraps GameObject.Instantiate(...) methods
92-
var instance = gameObjectFactory.Instantiate(prefab); // Prefab is created and injected
105+
[Inject] private readonly IScopeStore _scopeStore = default!;
106+
107+
var myObject = Instantiate(prefab);
108+
var myScript = myObject.AddComponent<MyScript>();
109+
110+
injecter.InjectIntoType(myScript, true);
111+
var disposer = myObject.AddComponent<MonoInjector>();
112+
disposer.Initialize(myScript, _scopeStore);
113+
114+
```
115+
116+
> [!Warning]
117+
> When doing this manually, the later injected script's `Awake` method might run before the injection happens
118+
119+
## Testing
120+
121+
You can load tests and prefabs with controlled dependencies when running tests inside Unity. To do this create the following class in your test assembly:
122+
123+
```csharp
124+
public static class InstallerStopper
125+
{
126+
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterAssembliesLoaded)]
127+
public static void DisableAppInstaller()
128+
{
129+
if (Environment.GetCommandLineArgs().Contains("-runTests")
130+
|| EditorWindow.HasOpenInstances<TestRunnerWindow>())
131+
{
132+
AppInstaller.Run = false;
133+
}
134+
}
135+
}
93136
```
94-
You don't have to call InjectIntoGameObject on prefab children. When InjectIntoGameObject is called all the scripts on the game object and it's children which have the InjectAttribute gets injected.
95137

96-
## Scopes, Disposables
138+
This will stop your `AppInstaller` from running when you execute tests. This will happen if either you have the test runner window open, or you are running Unity headless with the `-runTests` parameter (typically during CI).
97139

98-
- An IServiceScope is created for every script found in a GameObject.
99-
- Thus each MonoBehavior injected has it's own scope (Scoped lifetime services start from here).
100-
- A DestroyDetector script is added to every GameObject that receives injection. When the game object is destroyed, the DestroyDetector disposes of all the scopes that got created for that specific game object.
101-
- Thus if you create a prefab, destroy one of it's children then only the scopes associated with that child are disposed.
102-
- DestroyDetector is internal, and is hidden in the Inspector.
103-
- Destroying the game object holding the SceneInjector disposes of the IServiceProvider if it is disposable
104-
- When using the CompositionRoot behavior option the InjectedMonoBehavior handles disposing of the scope when destroyed and no DestroyDetector is created.
140+
> [!WARNING]
141+
> If you do this, then you must close the test runner window when entering play mode, otherwise the `AppInstaller` will not run
105142
106-
## Options
143+
In your tests set up the composition root manually
107144

108-
You can customize some behavior of the SceneInjector by providing an action to configure the options when calling AddSceneInjector
145+
```csharp
146+
[UnityTest]
147+
public IEnumerator My_Test_Does_Stuff()
148+
{
149+
CompositionRoot.ServiceProvider = new ServiceCollection()
150+
.AddTransient<IService, MyTestService>()
151+
.BuildServiceProvider();
152+
153+
// Do your tests, load scenes, prefabs, asserts etc...
154+
155+
CompositionRoot.ServiceProvider.Dispose();
156+
(CompositionRoot.ServiceProvider as IDisposable)?.Dispose();
157+
};
158+
```
109159

110-
Current options:
160+
> [!NOTE]
161+
> You can also do the same in the test's `Setup` or `Teardown` stage
111162
112-
| Name | Description | Default value|
113-
|---|---|---|
114-
| Behavior | CompositionRoot: Use the static service provider with inherited MonoBehaviors. Factory: use the SceneInjector and IGameObjectFactory | Factory |
115-
| DontDestroyOnLoad | Calls GameObject.DontDestroyOnLoad(SceneInjector) during initialization. This prevents the game object from being destroyed | True |
163+
## Migrating from `8.0.1` to `9.0.0`
116164

117-
## Notes
118-
- To see sample usage check out tests and test scenes
165+
1. Set up a composition root as described above.
166+
2. Remove inheriting from the old `MonoBehaviourInjected` and similar classes
167+
3. Optional - Decorate your injected scripts with `[RequireComponent(typeof(MonoInjector))]`
168+
4. In the editor press the `Tools / Injecter / Ensure injection scripts on everyting` button
169+
5. If a `GameObject` is missing the `MonoInjector` or `MonoDisposer` scripts, add them

Injecter.Build/Docs.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
using Nuke.Common;
22
using static Nuke.Common.Tools.DocFX.DocFXTasks;
33

4-
#pragma warning disable CA1822 // Mark members as static
54
sealed partial class Build
65
{
76
Target CreateMetadata => _ => _
@@ -18,4 +17,3 @@ sealed partial class Build
1817
.DependsOn(BuildDocs)
1918
.Executes(() => DocFX($"{DocFxJsonPath} --serve"));
2019
}
21-
#pragma warning restore CA1822 // Mark members as static

Injecter.Build/Injecter.Build.csproj

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,13 @@
1414
</PropertyGroup>
1515

1616
<ItemGroup>
17-
<PackageReference Include="Nuke.Common" Version="6.0.1" />
17+
<PackageReference Include="Nuke.Common" Version="6.2.1" />
1818
</ItemGroup>
1919

2020
<ItemGroup>
21-
<PackageDownload Include="docfx.console" Version="[2.59.0]" />
22-
<PackageDownload Include="dotnet-sonarscanner" Version="[5.5.3]" />
23-
<PackageDownload Include="NuGet.CommandLine" Version="[6.0.0]" />
21+
<PackageDownload Include="docfx.console" Version="[2.59.4]" />
22+
<PackageDownload Include="dotnet-sonarscanner" Version="[5.7.2]" />
23+
<PackageDownload Include="NuGet.CommandLine" Version="[6.2.1]" />
2424
<PackageDownload Include="nukeeper" Version="[0.35.0]" />
2525
</ItemGroup>
2626

Injecter.sln

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XamarinSample", "Samples\Xa
7474
EndProject
7575
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "XamarinSample", "XamarinSample", "{B70A83F3-3F9E-473E-8022-3B741B4137F5}"
7676
EndProject
77+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Injecter.Unity.Editor", "UnityProject\Proxies\Injecter.Unity.Editor\Injecter.Unity.Editor.csproj", "{6694D891-176C-4D30-A008-8B8E0833B8B1}"
78+
EndProject
7779
Global
7880
GlobalSection(SolutionConfigurationPlatforms) = preSolution
7981
Debug|Any CPU = Debug|Any CPU
@@ -209,6 +211,7 @@ Global
209211
{949C7767-02E5-454E-AC4D-3146BC27F7AE}.Release|x86.ActiveCfg = Release|Any CPU
210212
{949C7767-02E5-454E-AC4D-3146BC27F7AE}.Release|x86.Build.0 = Release|Any CPU
211213
{91B0FAF4-2783-420C-BA31-3CC94D1F8140}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
214+
{91B0FAF4-2783-420C-BA31-3CC94D1F8140}.Debug|Any CPU.Build.0 = Debug|Any CPU
212215
{91B0FAF4-2783-420C-BA31-3CC94D1F8140}.Debug|ARM.ActiveCfg = Debug|Any CPU
213216
{91B0FAF4-2783-420C-BA31-3CC94D1F8140}.Debug|ARM.Build.0 = Debug|Any CPU
214217
{91B0FAF4-2783-420C-BA31-3CC94D1F8140}.Debug|ARM64.ActiveCfg = Debug|Any CPU
@@ -218,6 +221,7 @@ Global
218221
{91B0FAF4-2783-420C-BA31-3CC94D1F8140}.Debug|x86.ActiveCfg = Debug|Any CPU
219222
{91B0FAF4-2783-420C-BA31-3CC94D1F8140}.Debug|x86.Build.0 = Debug|Any CPU
220223
{91B0FAF4-2783-420C-BA31-3CC94D1F8140}.Release|Any CPU.ActiveCfg = Release|Any CPU
224+
{91B0FAF4-2783-420C-BA31-3CC94D1F8140}.Release|Any CPU.Build.0 = Release|Any CPU
221225
{91B0FAF4-2783-420C-BA31-3CC94D1F8140}.Release|ARM.ActiveCfg = Release|Any CPU
222226
{91B0FAF4-2783-420C-BA31-3CC94D1F8140}.Release|ARM.Build.0 = Release|Any CPU
223227
{91B0FAF4-2783-420C-BA31-3CC94D1F8140}.Release|ARM64.ActiveCfg = Release|Any CPU
@@ -464,6 +468,26 @@ Global
464468
{70581949-DD5B-4D55-8D0B-F444E16CB871}.Release|x64.Build.0 = Release|Any CPU
465469
{70581949-DD5B-4D55-8D0B-F444E16CB871}.Release|x86.ActiveCfg = Release|Any CPU
466470
{70581949-DD5B-4D55-8D0B-F444E16CB871}.Release|x86.Build.0 = Release|Any CPU
471+
{6694D891-176C-4D30-A008-8B8E0833B8B1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
472+
{6694D891-176C-4D30-A008-8B8E0833B8B1}.Debug|Any CPU.Build.0 = Debug|Any CPU
473+
{6694D891-176C-4D30-A008-8B8E0833B8B1}.Debug|ARM.ActiveCfg = Debug|Any CPU
474+
{6694D891-176C-4D30-A008-8B8E0833B8B1}.Debug|ARM.Build.0 = Debug|Any CPU
475+
{6694D891-176C-4D30-A008-8B8E0833B8B1}.Debug|ARM64.ActiveCfg = Debug|Any CPU
476+
{6694D891-176C-4D30-A008-8B8E0833B8B1}.Debug|ARM64.Build.0 = Debug|Any CPU
477+
{6694D891-176C-4D30-A008-8B8E0833B8B1}.Debug|x64.ActiveCfg = Debug|Any CPU
478+
{6694D891-176C-4D30-A008-8B8E0833B8B1}.Debug|x64.Build.0 = Debug|Any CPU
479+
{6694D891-176C-4D30-A008-8B8E0833B8B1}.Debug|x86.ActiveCfg = Debug|Any CPU
480+
{6694D891-176C-4D30-A008-8B8E0833B8B1}.Debug|x86.Build.0 = Debug|Any CPU
481+
{6694D891-176C-4D30-A008-8B8E0833B8B1}.Release|Any CPU.ActiveCfg = Release|Any CPU
482+
{6694D891-176C-4D30-A008-8B8E0833B8B1}.Release|Any CPU.Build.0 = Release|Any CPU
483+
{6694D891-176C-4D30-A008-8B8E0833B8B1}.Release|ARM.ActiveCfg = Release|Any CPU
484+
{6694D891-176C-4D30-A008-8B8E0833B8B1}.Release|ARM.Build.0 = Release|Any CPU
485+
{6694D891-176C-4D30-A008-8B8E0833B8B1}.Release|ARM64.ActiveCfg = Release|Any CPU
486+
{6694D891-176C-4D30-A008-8B8E0833B8B1}.Release|ARM64.Build.0 = Release|Any CPU
487+
{6694D891-176C-4D30-A008-8B8E0833B8B1}.Release|x64.ActiveCfg = Release|Any CPU
488+
{6694D891-176C-4D30-A008-8B8E0833B8B1}.Release|x64.Build.0 = Release|Any CPU
489+
{6694D891-176C-4D30-A008-8B8E0833B8B1}.Release|x86.ActiveCfg = Release|Any CPU
490+
{6694D891-176C-4D30-A008-8B8E0833B8B1}.Release|x86.Build.0 = Release|Any CPU
467491
EndGlobalSection
468492
GlobalSection(SolutionProperties) = preSolution
469493
HideSolutionNode = FALSE
@@ -488,6 +512,7 @@ Global
488512
{9FA1A8F8-D2D9-4C46-84F5-6CA33697B846} = {B70A83F3-3F9E-473E-8022-3B741B4137F5}
489513
{70581949-DD5B-4D55-8D0B-F444E16CB871} = {B70A83F3-3F9E-473E-8022-3B741B4137F5}
490514
{B70A83F3-3F9E-473E-8022-3B741B4137F5} = {0CC35E76-EE92-4F84-890B-6B23C510A314}
515+
{6694D891-176C-4D30-A008-8B8E0833B8B1} = {4879E9FF-F766-4EDD-BDE0-679EE469E45D}
491516
EndGlobalSection
492517
GlobalSection(ExtensibilityGlobals) = postSolution
493518
SolutionGuid = {3F7ABD13-07BD-408C-8057-92D72C037CBA}

MainProject/Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<LangVersion>9.0</LangVersion>
44
<Nullable>enable</Nullable>
55

6-
<CurrentVersion>8.0.1</CurrentVersion>
6+
<CurrentVersion>9.0.0</CurrentVersion>
77

88
<Version>$(CurrentVersion)</Version>
99
<PakcageVersion>$(CurrentVersion)</PakcageVersion>

MainProject/Directory.Packages.props

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
<Project>
22
<ItemGroup>
3-
<PackageReference Include="Nullable" Version="1.3.0" PrivateAssets="all" />
3+
<PackageReference Include="Nullable" Version="1.3.1" PrivateAssets="all" />
44
</ItemGroup>
55

66
<ItemGroup>
7-
<PackageReference Include="Roslynator.Analyzers" Version="4.0.2" PrivateAssets="all" />
8-
<PackageReference Include="SonarAnalyzer.CSharp" Version="8.35.0.42613" PrivateAssets="all" />
7+
<PackageReference Include="Roslynator.Analyzers" Version="4.1.1" PrivateAssets="all" />
8+
<PackageReference Include="SonarAnalyzer.CSharp" Version="8.44.0.52574" PrivateAssets="all" />
99
</ItemGroup>
1010

1111
<ItemGroup>

MainProject/Injecter.Avalonia/InjectedUserControl.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ namespace Injecter.Avalonia
88
public abstract class InjectedUserControl : UserControl
99
{
1010
protected InjectedUserControl() =>
11-
Scope = CompositionRoot.ServiceProvider?.GetRequiredService<IInjecter>().InjectIntoType(GetType(), this, false);
11+
Scope = CompositionRoot.ServiceProvider?.GetRequiredService<IInjecter>().InjectIntoType(this, false);
1212

1313
protected IServiceScope? Scope { get; }
1414

MainProject/Injecter.Avalonia/InjectedWindow.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ namespace Injecter.Avalonia
88
public abstract class InjectedWindow : Window
99
{
1010
protected InjectedWindow() =>
11-
Scope = CompositionRoot.ServiceProvider?.GetRequiredService<IInjecter>().InjectIntoType(GetType(), this, false);
11+
Scope = CompositionRoot.ServiceProvider?.GetRequiredService<IInjecter>().InjectIntoType(this, false);
1212

1313
protected IServiceScope? Scope { get; }
1414

0 commit comments

Comments
 (0)