Skip to content

Commit

Permalink
Merge pull request #40 from mewlist/property-injection
Browse files Browse the repository at this point in the history
Property Injection
  • Loading branch information
mewlist authored Feb 21, 2024
2 parents 0d93491 + fb562d5 commit 7b6bb09
Show file tree
Hide file tree
Showing 14 changed files with 167 additions and 7 deletions.
2 changes: 1 addition & 1 deletion Runtime/Attribute/InjectAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

namespace Doinject
{
[AttributeUsage(AttributeTargets.Constructor|AttributeTargets.Method)]
[AttributeUsage(AttributeTargets.Constructor|AttributeTargets.Method|AttributeTargets.Field|AttributeTargets.Property)]
public class InjectAttribute : Attribute
{
}
Expand Down
17 changes: 17 additions & 0 deletions Runtime/DIContainer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ namespace Doinject
public class DIContainer : IReadOnlyDIContainer, IAsyncDisposable
{
private static Dictionary<Type, TargetMethodsInfo> MethodInfoMap { get; } = new();
private static Dictionary<Type, TargetPropertiesInfo> PropertyInfoMap { get; } = new();

private IReadOnlyDIContainer Parent { get; set; }
private Scene Scene { get; set; }
Expand All @@ -33,6 +34,7 @@ public class DIContainer : IReadOnlyDIContainer, IAsyncDisposable
internal ParameterBuilder ParameterBuilder { get; }
private ConstructorInjector ConstructorInjector { get; }
private MethodInjector MethodInjector { get; }
private PropertyInjector PropertyInjector { get; }

public IReadOnlyDictionary<TargetTypeInfo, IInternalResolver> ReadOnlyBindings => Resolvers;
internal IReadOnlyDictionary<TargetTypeInfo, ConcurrentObjectBag> ReadOnlyInstanceMap => ResolvedInstanceBag.ReadOnlyInstanceMap;
Expand All @@ -48,6 +50,7 @@ public DIContainer(IReadOnlyDIContainer parent = null, Scene scene = default)
ParameterBuilder = new ParameterBuilder(this);
ConstructorInjector = new ConstructorInjector(this);
MethodInjector = new MethodInjector(this);
PropertyInjector = new PropertyInjector(this);
BindFromInstance<IReadOnlyDIContainer, DIContainer>(this);
}

Expand Down Expand Up @@ -181,10 +184,12 @@ private async ValueTask<object> InstantiateInternalAsync(Type targetType, object

var target = await ConstructorInjector.DoInject(targetType, args, scopedInstances);
var targetMethodsInfo = GetTargetMethodInfo(targetType);
var targetPropertiesInfo = GetTargetPropertyInfo(targetType);

try
{
await MethodInjector.DoInject(target, targetMethodsInfo, args, scopedInstances);
await PropertyInjector.DoInject(target, targetPropertiesInfo);
}
catch (Exception e)
{
Expand Down Expand Up @@ -266,10 +271,12 @@ private async ValueTask InjectIntoInternalAsync<T>(T target, object[] args, Scop

var targetType = target.GetType();
var targetMethodsInfo = GetTargetMethodInfo(targetType);
var targetPropertiesInfo = GetTargetPropertyInfo(targetType);

try
{
await MethodInjector.DoInject(target, targetMethodsInfo, args, scopedInstances);
await PropertyInjector.DoInject(target, targetPropertiesInfo);
}
catch (Exception e)
{
Expand Down Expand Up @@ -397,6 +404,16 @@ private TargetMethodsInfo GetTargetMethodInfo(Type targetType)
return targetMethodsInfo;
}

private TargetPropertiesInfo GetTargetPropertyInfo(Type targetType)
{
if (PropertyInfoMap.TryGetValue(targetType, out var targetPropertiesInfo))
return targetPropertiesInfo;

targetPropertiesInfo = new TargetPropertiesInfo(targetType);
PropertyInfoMap[targetType] = targetPropertiesInfo;
return targetPropertiesInfo;
}

public async ValueTask DisposeAsync()
{
if (CancellationTokenSource.IsCancellationRequested) return;
Expand Down
5 changes: 1 addition & 4 deletions Runtime/Injector/MethodInjector.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using System.Threading.Tasks;
#if USE_UNITASK
using Cysharp.Threading.Tasks;
#endif
Expand Down
36 changes: 36 additions & 0 deletions Runtime/Injector/PropertyInjector.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using System.Threading.Tasks;

namespace Doinject
{
internal class PropertyInjector
{
private DIContainer Container { get; }
private ParameterBuilder ParameterBuilder { get; }

public PropertyInjector(DIContainer container)
{
Container = container;
ParameterBuilder = container.ParameterBuilder;
}

public async ValueTask DoInject<T>(
T target,
TargetPropertiesInfo properties)
{
if (!properties.Any()) return;
var targetType = target.GetType();
var resolverType = targetType;

if (targetType == typeof(IInjectableComponent))
resolverType = target.GetType();

Container.MarkInjected(resolverType);

foreach (var propertyInfo in properties.InjectProperties)
{
var instance = await Container.ResolveAsync(propertyInfo.PropertyType);
propertyInfo.SetValue(target, instance);
}
}
}
}
3 changes: 3 additions & 0 deletions Runtime/Injector/PropertyInjector.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 9 additions & 1 deletion Runtime/TargetMethodsInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,28 @@ internal class TargetMethodsInfo

public TargetMethodsInfo(Type targetType)
{
foreach (var methodInfo in targetType.GetMethods())
foreach (var methodInfo in targetType.GetMethods(BindingFlags.Instance|BindingFlags.Public|BindingFlags.NonPublic))
{
if (methodInfo.GetCustomAttributes(typeof(InjectAttribute), true).Length > 0)
{
if (!methodInfo.IsPublic)
throw new Exception($"Inject method must be public. {targetType.Name}.{methodInfo.Name}()");
InjectMethods.Add(methodInfo);
}

if (methodInfo.GetCustomAttributes(typeof(PostInjectAttribute), true).Length > 0)
{
if (!methodInfo.IsPublic)
throw new Exception($"PostInject method must be public. {targetType.Name}.{methodInfo.Name}()");
if (methodInfo.GetParameters().Length > 0)
throw new Exception("PostInject method should not have any parameters");
PostInjectMethods.Add(methodInfo);
}

if (methodInfo.GetCustomAttributes(typeof(OnInjectedAttribute), true).Length > 0)
{
if (!methodInfo.IsPublic)
throw new Exception($"OnInjected method must be public. {targetType.Name}.{methodInfo.Name}()");
if (methodInfo.GetParameters().Length > 0)
throw new Exception("OnInjected method should not have any parameters");
OnInjectedMethods.Add(methodInfo);
Expand Down
28 changes: 28 additions & 0 deletions Runtime/TargetPropertiesInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Reflection;

namespace Doinject
{
internal class TargetPropertiesInfo
{
public List<PropertyInfo> InjectProperties { get; } = new();

public TargetPropertiesInfo(Type targetType)
{
foreach (var propertyInfo in targetType.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
{
if (propertyInfo.GetCustomAttributes(typeof(InjectAttribute), true).Length <= 0)
continue;

if (!propertyInfo.CanWrite || !propertyInfo.SetMethod.IsPublic)
throw new Exception($"Property must have a public setter. {targetType.Name}.{propertyInfo.Name}");

InjectProperties.Add(propertyInfo);
}
}

public bool Any()
=> InjectProperties.Count > 0;
}
}
3 changes: 3 additions & 0 deletions Runtime/TargetPropertiesInfo.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions Tests/Components/PropertyInjectionComponent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using UnityEngine;

namespace Doinject.Tests
{
public class PropertyInjectionComponent : MonoBehaviour, IInjectableComponent
{
[Inject]
public InjectedObject InjectedObject { get; set; }
}
}
3 changes: 3 additions & 0 deletions Tests/Components/PropertyInjectionComponent.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions Tests/Components/PropertyInjectionWithNonPublicSetterComponent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using UnityEngine;

namespace Doinject.Tests
{
public class PropertyInjectionWithNonPublicSetterComponent : MonoBehaviour, IInjectableComponent
{
[Inject]
public InjectedObject InjectedObject { get; private set; }
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

39 changes: 38 additions & 1 deletion Tests/InjectionTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Threading.Tasks;
using Mew.Core.TaskHelpers;
using NUnit.Framework;
using UnityEngine.SceneManagement;

namespace Doinject.Tests
{
Expand All @@ -12,7 +13,8 @@ public class InjectionTest
[SetUp]
public void Setup()
{
container = new DIContainer();
var scene = SceneManager.GetActiveScene();
container = new DIContainer(parent: null, scene);
}

[TearDown]
Expand Down Expand Up @@ -124,5 +126,40 @@ public async Task InjectedCallbackTest()
await TaskHelperInternal.NextFrame();
Assert.That(instance.OnInjectedCalled, Is.True);
}

[Test]
public async Task PropertyInjectionTest()
{
container.BindTransient<PropertyInjectionObject>();
container.BindTransient<InjectedObject>();
var instance = await container.ResolveAsync<PropertyInjectionObject>();
Assert.NotNull(instance.InjectedObject);
}

[Test]
public async Task PropertyInjectionComponentTest()
{
container.BindTransient<PropertyInjectionComponent>();
container.BindTransient<InjectedObject>();
var instance = await container.ResolveAsync<PropertyInjectionComponent>();
Assert.NotNull(instance.InjectedObject);
}

[Test]
public async Task PropertyInjectionWithNonPublicSetterComponentTest()
{
container.BindTransient<PropertyInjectionWithNonPublicSetterComponent>();
container.BindTransient<InjectedObject>();
try
{
await container.ResolveAsync<PropertyInjectionWithNonPublicSetterComponent>();
}
catch (Exception e)
{
Assert.Pass();

}
Assert.Fail();
}
}
}
5 changes: 5 additions & 0 deletions Tests/TestObjects.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,11 @@ public OptionalInjectionTestClass([Optional]InjectedObject injectedObject)
}
}

public class PropertyInjectionObject
{
[Inject] public InjectedObject InjectedObject { get; set; }
}

public class InjectableObject : IDisposable
{
public InjectedObject InjectedObject { get; set; }
Expand Down

0 comments on commit 7b6bb09

Please sign in to comment.