Skip to content

Commit

Permalink
Adding service dependency injection feature and additional `TryGetSer…
Browse files Browse the repository at this point in the history
…vice` simplified call. (#111)
  • Loading branch information
SimonDarksideJ authored Oct 28, 2024
1 parent b072245 commit 170fc5e
Show file tree
Hide file tree
Showing 11 changed files with 299 additions and 0 deletions.
61 changes: 61 additions & 0 deletions Runtime/Services/ServiceManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -717,6 +718,12 @@ private bool TryCreateAndRegisterServiceInternal<T>(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
Expand Down Expand Up @@ -750,6 +757,50 @@ private bool TryCreateAndRegisterServiceInternal<T>(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;
}

/// <summary>
/// Service registration.
/// </summary>
Expand Down Expand Up @@ -1036,6 +1087,16 @@ public bool TryGetService<T>(out T service) where T : IService
return service != null;
}

/// <summary>
/// Retrieve the first <see cref="IService"/> from the <see cref="ActiveServices"/> that meets the selected type and name.
/// </summary>
/// <param name="interfaceType">Interface type of the service being requested.</param>
/// <param name="serviceInstance">return parameter of the function.</param>
public bool TryGetService(Type interfaceType, out IService service)
{
return TryGetService(interfaceType, string.Empty, out service);
}

/// <summary>
/// Retrieve the first <see cref="IService"/> from the <see cref="ActiveServices"/> that meets the selected type and name.
/// </summary>
Expand Down
7 changes: 7 additions & 0 deletions Tests/TestData/Interfaces/ITestDependencyService1.cs
Original file line number Diff line number Diff line change
@@ -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 { }
}
11 changes: 11 additions & 0 deletions Tests/TestData/Interfaces/ITestDependencyService1.cs.meta

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

7 changes: 7 additions & 0 deletions Tests/TestData/Interfaces/ITestDependencyService2.cs
Original file line number Diff line number Diff line change
@@ -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 { }
}
11 changes: 11 additions & 0 deletions Tests/TestData/Interfaces/ITestDependencyService2.cs.meta

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

30 changes: 30 additions & 0 deletions Tests/TestData/Services/DependencyTestService1.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
11 changes: 11 additions & 0 deletions Tests/TestData/Services/DependencyTestService1.cs.meta

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

32 changes: 32 additions & 0 deletions Tests/TestData/Services/DependencyTestService2.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
11 changes: 11 additions & 0 deletions Tests/TestData/Services/DependencyTestService2.cs.meta

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

107 changes: 107 additions & 0 deletions Tests/Tests/Service Dependency Tests.cs
Original file line number Diff line number Diff line change
@@ -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<ITestService1>(typeof(TestService1), TestService1.TestName, 1, AllPlatforms.Platforms, null);
var serviceResult = testServiceManager.TryCreateAndRegisterService<ITestService1>(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<ITestService1>(typeof(TestService1), TestService1.TestName, 1, AllPlatforms.Platforms, null);
testServiceManager.TryCreateAndRegisterService<ITestService1>(config, out ITestService1 testService1);

var config2 = new ServiceConfiguration<ITestDependencyService1>(typeof(DependencyTestService1), "Dependency Service", 1, AllPlatforms.Platforms, null);
var serviceResult2 = testServiceManager.TryCreateAndRegisterService<ITestDependencyService1>(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<ITestDependencyService1>(typeof(DependencyTestService1), "Dependency Service", 1, AllPlatforms.Platforms, null);
var serviceResult2 = testServiceManager.TryCreateAndRegisterService<ITestDependencyService1>(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<ITestService1>(typeof(TestService1), TestService1.TestName, 1, AllPlatforms.Platforms, null);
testServiceManager.TryCreateAndRegisterService<ITestService1>(config, out ITestService1 testService1);

var config2 = new ServiceConfiguration<ITestDependencyService1>(typeof(DependencyTestService1), "Dependency Service", 1, AllPlatforms.Platforms, null);
testServiceManager.TryCreateAndRegisterService<ITestDependencyService1>(config2, out ITestDependencyService1 testService2);

var config3 = new ServiceConfiguration<ITestDependencyService2>(typeof(DependencyTestService2), "Dependency Service", 1, AllPlatforms.Platforms, null);
var serviceResult3 = testServiceManager.TryCreateAndRegisterService<ITestDependencyService2>(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
}
}
11 changes: 11 additions & 0 deletions Tests/Tests/Service Dependency Tests.cs.meta

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

0 comments on commit 170fc5e

Please sign in to comment.