From 170fc5e7b54847469dd6bc2f4e6b2907c2713c64 Mon Sep 17 00:00:00 2001 From: "Simon (Darkside) Jackson" Date: Mon, 28 Oct 2024 14:00:20 +0000 Subject: [PATCH] Adding service dependency injection feature and additional `TryGetService` simplified call. (#111) --- Runtime/Services/ServiceManager.cs | 61 ++++++++++ .../Interfaces/ITestDependencyService1.cs | 7 ++ .../ITestDependencyService1.cs.meta | 11 ++ .../Interfaces/ITestDependencyService2.cs | 7 ++ .../ITestDependencyService2.cs.meta | 11 ++ .../Services/DependencyTestService1.cs | 30 +++++ .../Services/DependencyTestService1.cs.meta | 11 ++ .../Services/DependencyTestService2.cs | 32 ++++++ .../Services/DependencyTestService2.cs.meta | 11 ++ Tests/Tests/Service Dependency Tests.cs | 107 ++++++++++++++++++ Tests/Tests/Service Dependency Tests.cs.meta | 11 ++ 11 files changed, 299 insertions(+) create mode 100644 Tests/TestData/Interfaces/ITestDependencyService1.cs create mode 100644 Tests/TestData/Interfaces/ITestDependencyService1.cs.meta create mode 100644 Tests/TestData/Interfaces/ITestDependencyService2.cs create mode 100644 Tests/TestData/Interfaces/ITestDependencyService2.cs.meta create mode 100644 Tests/TestData/Services/DependencyTestService1.cs create mode 100644 Tests/TestData/Services/DependencyTestService1.cs.meta create mode 100644 Tests/TestData/Services/DependencyTestService2.cs create mode 100644 Tests/TestData/Services/DependencyTestService2.cs.meta create mode 100644 Tests/Tests/Service Dependency Tests.cs create mode 100644 Tests/Tests/Service Dependency Tests.cs.meta diff --git a/Runtime/Services/ServiceManager.cs b/Runtime/Services/ServiceManager.cs index 037fa61..0330aa5 100644 --- a/Runtime/Services/ServiceManager.cs +++ b/Runtime/Services/ServiceManager.cs @@ -10,6 +10,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Reflection; using System.Threading.Tasks; using UnityEngine; using UnityEngine.EventSystems; @@ -717,6 +718,12 @@ private bool TryCreateAndRegisterServiceInternal(Type concreteType, IReadOnly return false; } + if (!TryInjectDependentServices(concreteType, ref args)) + { + Debug.LogError($"Failed to register the {concreteType.Name} service due to missing dependencies. Ensure all dependencies are registered prior to registering this service."); + return false; + } + IService serviceInstance; try @@ -750,6 +757,50 @@ private bool TryCreateAndRegisterServiceInternal(Type concreteType, IReadOnly return TryRegisterService(typeof(T), serviceInstance); } + private bool TryInjectDependentServices(Type concreteType, ref object[] args) + { + args ??= new object[0]; + + ConstructorInfo[] constructors = concreteType.GetConstructors(); + if (constructors.Length == 0) + { + Debug.LogError($"Failed to find a constructor for {concreteType.Name}!"); + return false; + } + + // we are only focusing on the primary constructor for now. + var primaryConstructor = constructors[0]; + + ParameterInfo[] parameters = primaryConstructor.GetParameters(); + + // If there are no additional dependencies other than the base 3 (Name, Priority, Profile), then we can skip this. + if (parameters.Length == 0 || parameters.Length == args.Length) + { + return true; + } + + Debug.Log($"Constructor: {primaryConstructor}"); + + foreach (var parameter in parameters) + { + if (parameter.ParameterType.IsInterface && typeof(IService).IsAssignableFrom(parameter.ParameterType)) + { + if (TryGetService(parameter.ParameterType, out var dependency)) + { + Debug.Log($"Injecting {dependency.Name} into {parameter.Name}"); + args = args.AddItem(dependency); + } + else + { + Debug.LogError($"Failed to find an {parameter.ParameterType.Name} service to inject into {parameter.Name}!"); + } + } + } + + // Does the args length match the resolved argument count, base Arguments plus resolved dependencies. + return parameters.Length == args.Length; + } + /// /// Service registration. /// @@ -1036,6 +1087,16 @@ public bool TryGetService(out T service) where T : IService return service != null; } + /// + /// Retrieve the first from the that meets the selected type and name. + /// + /// Interface type of the service being requested. + /// return parameter of the function. + public bool TryGetService(Type interfaceType, out IService service) + { + return TryGetService(interfaceType, string.Empty, out service); + } + /// /// Retrieve the first from the that meets the selected type and name. /// diff --git a/Tests/TestData/Interfaces/ITestDependencyService1.cs b/Tests/TestData/Interfaces/ITestDependencyService1.cs new file mode 100644 index 0000000..c1c1e2e --- /dev/null +++ b/Tests/TestData/Interfaces/ITestDependencyService1.cs @@ -0,0 +1,7 @@ +// Copyright (c) Reality Collective. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +namespace RealityCollective.ServiceFramework.Tests.Interfaces +{ + public interface ITestDependencyService1 : ITestService { } +} \ No newline at end of file diff --git a/Tests/TestData/Interfaces/ITestDependencyService1.cs.meta b/Tests/TestData/Interfaces/ITestDependencyService1.cs.meta new file mode 100644 index 0000000..16486b5 --- /dev/null +++ b/Tests/TestData/Interfaces/ITestDependencyService1.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1e3d9b661b1d4924c8317c9deb4ae4bd +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/TestData/Interfaces/ITestDependencyService2.cs b/Tests/TestData/Interfaces/ITestDependencyService2.cs new file mode 100644 index 0000000..6a4e493 --- /dev/null +++ b/Tests/TestData/Interfaces/ITestDependencyService2.cs @@ -0,0 +1,7 @@ +// Copyright (c) Reality Collective. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +namespace RealityCollective.ServiceFramework.Tests.Interfaces +{ + public interface ITestDependencyService2 : ITestService { } +} \ No newline at end of file diff --git a/Tests/TestData/Interfaces/ITestDependencyService2.cs.meta b/Tests/TestData/Interfaces/ITestDependencyService2.cs.meta new file mode 100644 index 0000000..31b54b6 --- /dev/null +++ b/Tests/TestData/Interfaces/ITestDependencyService2.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8279b734f117b534fbe064b5816f7371 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/TestData/Services/DependencyTestService1.cs b/Tests/TestData/Services/DependencyTestService1.cs new file mode 100644 index 0000000..c5d1f86 --- /dev/null +++ b/Tests/TestData/Services/DependencyTestService1.cs @@ -0,0 +1,30 @@ +// Copyright (c) Reality Collective. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using RealityCollective.ServiceFramework.Definitions; +using RealityCollective.ServiceFramework.Services; +using RealityCollective.ServiceFramework.Tests.Interfaces; +using UnityEngine; + +namespace RealityCollective.ServiceFramework.Tests.Services +{ + public class DependencyTestService1 : BaseServiceWithConstructor, ITestDependencyService1 + { + public const string TestName = "Dependency Test Service 1"; + public ITestService1 testService1; + + public DependencyTestService1(string name, uint priority, BaseProfile profile, ITestService1 testService1) + : base(name, priority) + { + this.testService1 = testService1; + } + + public override void Initialize() + { + //base.Initialize(); + Debug.Log($"{TestName} is Initialised"); + } + + public override bool RegisterServiceModules => false; + } +} \ No newline at end of file diff --git a/Tests/TestData/Services/DependencyTestService1.cs.meta b/Tests/TestData/Services/DependencyTestService1.cs.meta new file mode 100644 index 0000000..fd841cd --- /dev/null +++ b/Tests/TestData/Services/DependencyTestService1.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 565e01424423d754ca5e91d6bf76293e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/TestData/Services/DependencyTestService2.cs b/Tests/TestData/Services/DependencyTestService2.cs new file mode 100644 index 0000000..5aaabaf --- /dev/null +++ b/Tests/TestData/Services/DependencyTestService2.cs @@ -0,0 +1,32 @@ +// Copyright (c) Reality Collective. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using RealityCollective.ServiceFramework.Definitions; +using RealityCollective.ServiceFramework.Services; +using RealityCollective.ServiceFramework.Tests.Interfaces; +using UnityEngine; + +namespace RealityCollective.ServiceFramework.Tests.Services +{ + public class DependencyTestService2 : BaseServiceWithConstructor, ITestDependencyService2 + { + public const string TestName = "Dependency Test Service 2"; + public ITestService1 testService1; + public ITestDependencyService1 testService2; + + public DependencyTestService2(string name, uint priority, BaseProfile profile, ITestService1 testService1, ITestDependencyService1 testService2) + : base(name, priority) + { + this.testService1 = testService1; + this.testService2 = testService2; + } + + public override void Initialize() + { + //base.Initialize(); + Debug.Log($"{TestName} is Initialised"); + } + + public override bool RegisterServiceModules => false; + } +} \ No newline at end of file diff --git a/Tests/TestData/Services/DependencyTestService2.cs.meta b/Tests/TestData/Services/DependencyTestService2.cs.meta new file mode 100644 index 0000000..7a3afc7 --- /dev/null +++ b/Tests/TestData/Services/DependencyTestService2.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8042b6168286ece4cb498a3c24ef4149 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Tests/Service Dependency Tests.cs b/Tests/Tests/Service Dependency Tests.cs new file mode 100644 index 0000000..064f8ee --- /dev/null +++ b/Tests/Tests/Service Dependency Tests.cs @@ -0,0 +1,107 @@ +// Copyright (c) Reality Collective. All rights reserved. + +using NUnit.Framework; +using RealityCollective.ServiceFramework.Definitions; +using RealityCollective.ServiceFramework.Definitions.Platforms; +using RealityCollective.ServiceFramework.Interfaces; +using RealityCollective.ServiceFramework.Services; +using RealityCollective.ServiceFramework.Tests.Interfaces; +using RealityCollective.ServiceFramework.Tests.Services; +using RealityCollective.ServiceFramework.Tests.Utilities; +using System.Text.RegularExpressions; +using UnityEngine; +using UnityEngine.TestTools; + +namespace RealityCollective.ServiceFramework.Tests.N_ServiceDependency +{ + internal class ServiceDependencyTests + { + private ServiceManager testServiceManager; + + #region Service Retrieval + + [Test] + public void Test_10_01_ParentServiceRegistration() + { + TestUtilities.InitializeServiceManagerScene(ref testServiceManager); + + var activeServiceCount = testServiceManager.ActiveServices.Count; + + var config = new ServiceConfiguration(typeof(TestService1), TestService1.TestName, 1, AllPlatforms.Platforms, null); + var serviceResult = testServiceManager.TryCreateAndRegisterService(config, out ITestService1 testService1); + + // Tests + Assert.IsTrue(serviceResult, "Test service was not registered"); + Assert.IsNotNull(testService1, "Test service instance was not returned"); + Assert.IsTrue(testService1.ServiceGuid != System.Guid.Empty, "No GUID generated for the test service"); + Assert.AreEqual(activeServiceCount + 1, testServiceManager.ActiveServices.Count, "More or less services found than was expected"); + } + + [Test] + public void Test_10_02_DependentServiceRegistration() + { + TestUtilities.InitializeServiceManagerScene(ref testServiceManager); + + var activeServiceCount = testServiceManager.ActiveServices.Count; + + var config = new ServiceConfiguration(typeof(TestService1), TestService1.TestName, 1, AllPlatforms.Platforms, null); + testServiceManager.TryCreateAndRegisterService(config, out ITestService1 testService1); + + var config2 = new ServiceConfiguration(typeof(DependencyTestService1), "Dependency Service", 1, AllPlatforms.Platforms, null); + var serviceResult2 = testServiceManager.TryCreateAndRegisterService(config2, out ITestDependencyService1 testService2); + + // Tests + Assert.IsTrue(serviceResult2, "Test service 2 was not registered"); + Assert.IsNotNull(testService2, "Test service 2 instance was not returned"); + Assert.IsTrue(testService2.ServiceGuid == System.Guid.Empty, "GUID found for the second test service when none configured"); + Assert.AreEqual(activeServiceCount + 2, testServiceManager.ActiveServices.Count, "More or less services found than was expected"); + Assert.IsNotNull((testService2 as DependencyTestService1)?.testService1, "Dependent service was not injected with parent service"); + } + + [Test] + public void Test_10_03_DependentServiceRegistrationMissingDependency() + { + TestUtilities.InitializeServiceManagerScene(ref testServiceManager); + + var activeServiceCount = testServiceManager.ActiveServices.Count; + + var config2 = new ServiceConfiguration(typeof(DependencyTestService1), "Dependency Service", 1, AllPlatforms.Platforms, null); + var serviceResult2 = testServiceManager.TryCreateAndRegisterService(config2, out ITestDependencyService1 testService2); + LogAssert.Expect(LogType.Error, new Regex("Failed to find an ITestService1 service to inject into testService1!")); + LogAssert.Expect(LogType.Error, new Regex("Failed to register the DependencyTestService1 service due to missing dependencies. Ensure all dependencies are registered prior to registering this service.")); + + // Tests + Assert.IsFalse(serviceResult2, "Test service 2 was registered, when it should not have been"); + Assert.IsNull(testService2, "Test service 2 instance was returned"); + Assert.AreEqual(activeServiceCount, testServiceManager.ActiveServices.Count, "More or less services found than was expected"); + Assert.IsNull((testService2 as DependencyTestService1)?.testService1, "Dependent service was not injected with parent service"); + } + + [Test] + public void Test_10_04_MultipleDependentServiceRegistration() + { + TestUtilities.InitializeServiceManagerScene(ref testServiceManager); + + var activeServiceCount = testServiceManager.ActiveServices.Count; + + var config = new ServiceConfiguration(typeof(TestService1), TestService1.TestName, 1, AllPlatforms.Platforms, null); + testServiceManager.TryCreateAndRegisterService(config, out ITestService1 testService1); + + var config2 = new ServiceConfiguration(typeof(DependencyTestService1), "Dependency Service", 1, AllPlatforms.Platforms, null); + testServiceManager.TryCreateAndRegisterService(config2, out ITestDependencyService1 testService2); + + var config3 = new ServiceConfiguration(typeof(DependencyTestService2), "Dependency Service", 1, AllPlatforms.Platforms, null); + var serviceResult3 = testServiceManager.TryCreateAndRegisterService(config3, out ITestDependencyService2 testService3); + + // Tests + Assert.IsTrue(serviceResult3, "Test service 3 was not registered"); + Assert.IsNotNull(testService3, "Test service 3 instance was not returned"); + Assert.IsTrue(testService3.ServiceGuid == System.Guid.Empty, "GUID found for the second test service when none configured"); + Assert.AreEqual(activeServiceCount + 3, testServiceManager.ActiveServices.Count, "More or less services found than was expected"); + Assert.IsNotNull((testService3 as DependencyTestService2)?.testService1, "Dependent service was not injected with parent service"); + Assert.IsNotNull((testService3 as DependencyTestService2)?.testService2, "Dependent service was not injected with parent service"); + } + + #endregion Service Retrieval + } +} \ No newline at end of file diff --git a/Tests/Tests/Service Dependency Tests.cs.meta b/Tests/Tests/Service Dependency Tests.cs.meta new file mode 100644 index 0000000..9090f81 --- /dev/null +++ b/Tests/Tests/Service Dependency Tests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 668549c1580871d40b91ac8202ddad05 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: