From 672e3aad0c32c9df1a09022c02f3aaa6f3335b5b Mon Sep 17 00:00:00 2001 From: mewlist Date: Wed, 21 Feb 2024 08:03:20 +0900 Subject: [PATCH 1/2] Property Injection --- Runtime/Attribute/InjectAttribute.cs | 2 +- Runtime/DIContainer.cs | 17 +++++++++ Runtime/Injector/MethodInjector.cs | 5 +-- Runtime/Injector/PropertyInjector.cs | 36 +++++++++++++++++++ Runtime/Injector/PropertyInjector.cs.meta | 3 ++ Runtime/TargetPropertiesInfo.cs | 23 ++++++++++++ Runtime/TargetPropertiesInfo.cs.meta | 3 ++ .../Components/PropertyInjectionComponent.cs | 10 ++++++ .../PropertyInjectionComponent.cs.meta | 3 ++ Tests/InjectionTest.cs | 22 +++++++++++- Tests/TestObjects.cs | 5 +++ 11 files changed, 123 insertions(+), 6 deletions(-) create mode 100644 Runtime/Injector/PropertyInjector.cs create mode 100644 Runtime/Injector/PropertyInjector.cs.meta create mode 100644 Runtime/TargetPropertiesInfo.cs create mode 100644 Runtime/TargetPropertiesInfo.cs.meta create mode 100644 Tests/Components/PropertyInjectionComponent.cs create mode 100644 Tests/Components/PropertyInjectionComponent.cs.meta diff --git a/Runtime/Attribute/InjectAttribute.cs b/Runtime/Attribute/InjectAttribute.cs index 01e14a0..772e5c1 100644 --- a/Runtime/Attribute/InjectAttribute.cs +++ b/Runtime/Attribute/InjectAttribute.cs @@ -2,7 +2,7 @@ namespace Doinject { - [AttributeUsage(AttributeTargets.Constructor|AttributeTargets.Method)] + [AttributeUsage(AttributeTargets.Constructor|AttributeTargets.Method|AttributeTargets.Field|AttributeTargets.Property)] public class InjectAttribute : Attribute { } diff --git a/Runtime/DIContainer.cs b/Runtime/DIContainer.cs index cbbd999..e4f2b90 100644 --- a/Runtime/DIContainer.cs +++ b/Runtime/DIContainer.cs @@ -20,6 +20,7 @@ namespace Doinject public class DIContainer : IReadOnlyDIContainer, IAsyncDisposable { private static Dictionary MethodInfoMap { get; } = new(); + private static Dictionary PropertyInfoMap { get; } = new(); private IReadOnlyDIContainer Parent { get; set; } private Scene Scene { get; set; } @@ -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 ReadOnlyBindings => Resolvers; internal IReadOnlyDictionary ReadOnlyInstanceMap => ResolvedInstanceBag.ReadOnlyInstanceMap; @@ -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(this); } @@ -181,10 +184,12 @@ private async ValueTask 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) { @@ -266,10 +271,12 @@ private async ValueTask InjectIntoInternalAsync(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) { @@ -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; diff --git a/Runtime/Injector/MethodInjector.cs b/Runtime/Injector/MethodInjector.cs index 1d1ae25..ea5f8d8 100644 --- a/Runtime/Injector/MethodInjector.cs +++ b/Runtime/Injector/MethodInjector.cs @@ -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 diff --git a/Runtime/Injector/PropertyInjector.cs b/Runtime/Injector/PropertyInjector.cs new file mode 100644 index 0000000..c663e3c --- /dev/null +++ b/Runtime/Injector/PropertyInjector.cs @@ -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 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); + } + } + } +} \ No newline at end of file diff --git a/Runtime/Injector/PropertyInjector.cs.meta b/Runtime/Injector/PropertyInjector.cs.meta new file mode 100644 index 0000000..8273f64 --- /dev/null +++ b/Runtime/Injector/PropertyInjector.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 905125ad9969408d916bb4ba66cdc923 +timeCreated: 1708469119 \ No newline at end of file diff --git a/Runtime/TargetPropertiesInfo.cs b/Runtime/TargetPropertiesInfo.cs new file mode 100644 index 0000000..cc77f8b --- /dev/null +++ b/Runtime/TargetPropertiesInfo.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Reflection; + +namespace Doinject +{ + internal class TargetPropertiesInfo + { + public List InjectProperties { get; } = new(); + + public TargetPropertiesInfo(Type targetType) + { + foreach (var propertyInfo in targetType.GetProperties()) + { + if (propertyInfo.GetCustomAttributes(typeof(InjectAttribute), true).Length > 0) + InjectProperties.Add(propertyInfo); + } + } + + public bool Any() + => InjectProperties.Count > 0; + } +} \ No newline at end of file diff --git a/Runtime/TargetPropertiesInfo.cs.meta b/Runtime/TargetPropertiesInfo.cs.meta new file mode 100644 index 0000000..e4f68a5 --- /dev/null +++ b/Runtime/TargetPropertiesInfo.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: cdd497cc987548c79f3120fe1c9b9396 +timeCreated: 1708469046 \ No newline at end of file diff --git a/Tests/Components/PropertyInjectionComponent.cs b/Tests/Components/PropertyInjectionComponent.cs new file mode 100644 index 0000000..24606d3 --- /dev/null +++ b/Tests/Components/PropertyInjectionComponent.cs @@ -0,0 +1,10 @@ +using UnityEngine; + +namespace Doinject.Tests +{ + public class PropertyInjectionComponent : MonoBehaviour, IInjectableComponent + { + [Inject] + public InjectedObject InjectedObject { get; private set; } + } +} \ No newline at end of file diff --git a/Tests/Components/PropertyInjectionComponent.cs.meta b/Tests/Components/PropertyInjectionComponent.cs.meta new file mode 100644 index 0000000..0672bf3 --- /dev/null +++ b/Tests/Components/PropertyInjectionComponent.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: e3e94a1fd8d84a9dbc431e6dad89705f +timeCreated: 1708469590 \ No newline at end of file diff --git a/Tests/InjectionTest.cs b/Tests/InjectionTest.cs index 80f88d8..4683819 100644 --- a/Tests/InjectionTest.cs +++ b/Tests/InjectionTest.cs @@ -2,6 +2,7 @@ using System.Threading.Tasks; using Mew.Core.TaskHelpers; using NUnit.Framework; +using UnityEngine.SceneManagement; namespace Doinject.Tests { @@ -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] @@ -124,5 +126,23 @@ public async Task InjectedCallbackTest() await TaskHelperInternal.NextFrame(); Assert.That(instance.OnInjectedCalled, Is.True); } + + [Test] + public async Task PropertyInjectionTest() + { + container.BindTransient(); + container.BindTransient(); + var instance = await container.ResolveAsync(); + Assert.NotNull(instance.InjectedObject); + } + + [Test] + public async Task PropertyInjectionComponentTest() + { + container.BindTransient(); + container.BindTransient(); + var instance = await container.ResolveAsync(); + Assert.NotNull(instance.InjectedObject); + } } } \ No newline at end of file diff --git a/Tests/TestObjects.cs b/Tests/TestObjects.cs index 0ce61b3..d038824 100644 --- a/Tests/TestObjects.cs +++ b/Tests/TestObjects.cs @@ -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; } From fb562d593e83384b8d7c72efbbf38fe8c9fa30ee Mon Sep 17 00:00:00 2001 From: mewlist Date: Wed, 21 Feb 2024 08:59:51 +0900 Subject: [PATCH 2/2] Injection point must be public --- Runtime/TargetMethodsInfo.cs | 10 +++++++++- Runtime/TargetPropertiesInfo.cs | 11 ++++++++--- Tests/Components/PropertyInjectionComponent.cs | 2 +- ...ertyInjectionWithNonPublicSetterComponent.cs | 10 ++++++++++ ...njectionWithNonPublicSetterComponent.cs.meta | 3 +++ Tests/InjectionTest.cs | 17 +++++++++++++++++ 6 files changed, 48 insertions(+), 5 deletions(-) create mode 100644 Tests/Components/PropertyInjectionWithNonPublicSetterComponent.cs create mode 100644 Tests/Components/PropertyInjectionWithNonPublicSetterComponent.cs.meta diff --git a/Runtime/TargetMethodsInfo.cs b/Runtime/TargetMethodsInfo.cs index f2eaedd..da8218e 100644 --- a/Runtime/TargetMethodsInfo.cs +++ b/Runtime/TargetMethodsInfo.cs @@ -12,13 +12,19 @@ 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); @@ -26,6 +32,8 @@ public TargetMethodsInfo(Type targetType) 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); diff --git a/Runtime/TargetPropertiesInfo.cs b/Runtime/TargetPropertiesInfo.cs index cc77f8b..80567a1 100644 --- a/Runtime/TargetPropertiesInfo.cs +++ b/Runtime/TargetPropertiesInfo.cs @@ -10,10 +10,15 @@ internal class TargetPropertiesInfo public TargetPropertiesInfo(Type targetType) { - foreach (var propertyInfo in targetType.GetProperties()) + foreach (var propertyInfo in targetType.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) { - if (propertyInfo.GetCustomAttributes(typeof(InjectAttribute), true).Length > 0) - InjectProperties.Add(propertyInfo); + 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); } } diff --git a/Tests/Components/PropertyInjectionComponent.cs b/Tests/Components/PropertyInjectionComponent.cs index 24606d3..81f3c27 100644 --- a/Tests/Components/PropertyInjectionComponent.cs +++ b/Tests/Components/PropertyInjectionComponent.cs @@ -5,6 +5,6 @@ namespace Doinject.Tests public class PropertyInjectionComponent : MonoBehaviour, IInjectableComponent { [Inject] - public InjectedObject InjectedObject { get; private set; } + public InjectedObject InjectedObject { get; set; } } } \ No newline at end of file diff --git a/Tests/Components/PropertyInjectionWithNonPublicSetterComponent.cs b/Tests/Components/PropertyInjectionWithNonPublicSetterComponent.cs new file mode 100644 index 0000000..31bb822 --- /dev/null +++ b/Tests/Components/PropertyInjectionWithNonPublicSetterComponent.cs @@ -0,0 +1,10 @@ +using UnityEngine; + +namespace Doinject.Tests +{ + public class PropertyInjectionWithNonPublicSetterComponent : MonoBehaviour, IInjectableComponent + { + [Inject] + public InjectedObject InjectedObject { get; private set; } + } +} \ No newline at end of file diff --git a/Tests/Components/PropertyInjectionWithNonPublicSetterComponent.cs.meta b/Tests/Components/PropertyInjectionWithNonPublicSetterComponent.cs.meta new file mode 100644 index 0000000..ec73184 --- /dev/null +++ b/Tests/Components/PropertyInjectionWithNonPublicSetterComponent.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: c9eebc0e77444858bd0b3c358ec68792 +timeCreated: 1708473725 \ No newline at end of file diff --git a/Tests/InjectionTest.cs b/Tests/InjectionTest.cs index 4683819..e937392 100644 --- a/Tests/InjectionTest.cs +++ b/Tests/InjectionTest.cs @@ -144,5 +144,22 @@ public async Task PropertyInjectionComponentTest() var instance = await container.ResolveAsync(); Assert.NotNull(instance.InjectedObject); } + + [Test] + public async Task PropertyInjectionWithNonPublicSetterComponentTest() + { + container.BindTransient(); + container.BindTransient(); + try + { + await container.ResolveAsync(); + } + catch (Exception e) + { + Assert.Pass(); + + } + Assert.Fail(); + } } } \ No newline at end of file