diff --git a/Syrette.Tests/ServiceContainerConstructorSelectionTests.cs b/Syrette.Tests/ServiceContainerConstructorSelectionTests.cs new file mode 100644 index 0000000..e5dc555 --- /dev/null +++ b/Syrette.Tests/ServiceContainerConstructorSelectionTests.cs @@ -0,0 +1,127 @@ +namespace Syrette.Tests; + +public class ServiceContainerConstructorSelectionTests +{ + public class CtorWithOptional + { + public ITestService? A { get; } + public string? Name { get; } + + public CtorWithOptional(ITestService a, string? name = "default") + { + A = a; + Name = name; + } + } + + public class CtorWithExactMatch + { + public int Value { get; } + + public CtorWithExactMatch(int value) { Value = value; } + public CtorWithExactMatch(int value, string label) { Value = value; } + } + + public class NoSatisfiableCtor + { + public NoSatisfiableCtor(int value) { } + } + + [Fact] + public void Greedy_picks_most_satisfiable_constructor() + { + var container = new ServiceContainer(); + container.AddSingleton(); + container.AddSingleton(); + + container.AddSingleton(); + var instance = container.GetService(); + + Assert.NotNull(instance.A); + Assert.NotNull(instance.B); + Assert.Null(instance.C); + } + + [Fact] + public void Greedy_picks_all_satisfied_over_partial() + { + var container = new ServiceContainer(); + container.AddSingleton(); + container.AddSingleton(); + container.AddSingleton(); + + container.AddSingleton(); + var instance = container.GetService(); + + Assert.NotNull(instance.A); + Assert.NotNull(instance.B); + Assert.NotNull(instance.C); + } + + [Fact] + public void Greedy_falls_back_to_1_param_when_only_1_satisfiable() + { + var container = new ServiceContainer(); + container.AddSingleton(); + + container.AddSingleton(); + var instance = container.GetService(); + + Assert.NotNull(instance.A); + Assert.Null(instance.B); + Assert.Null(instance.C); + } + + [Fact] + public void Optional_parameter_used_when_not_registered() + { + var container = new ServiceContainer(); + container.AddSingleton(); + container.AddSingleton(); + + var instance = container.GetService(); + Assert.NotNull(instance.A); + Assert.Equal("default", instance.Name); + } + + [Fact] + public void Registration_args_satisfy_constructor() + { + var container = new ServiceContainer(); + container.AddSingleton(42); + + var instance = container.GetService(); + Assert.Equal(42, instance.Value); + } + + [Fact] + public void Resolution_args_satisfy_constructor() + { + var container = new ServiceContainer(); + container.AddSingleton(); + + var instance = (CtorWithExactMatch)container.GetService( + typeof(CtorWithExactMatch), new object[] { 99 }); + Assert.Equal(99, instance.Value); + } + + [Fact] + public void Resolution_args_override_registration_args() + { + var container = new ServiceContainer(); + container.AddSingleton(10); + + var instance = (CtorWithExactMatch)container.GetService( + typeof(CtorWithExactMatch), new object[] { 20 }); + Assert.Equal(20, instance.Value); + } + + [Fact] + public void No_suitable_constructor_throws() + { + var container = new ServiceContainer(); + container.AddSingleton(); + Assert.Throws(() => + container.GetService()); + } +} diff --git a/Syrette.Tests/ServiceContainerDisposalTests.cs b/Syrette.Tests/ServiceContainerDisposalTests.cs new file mode 100644 index 0000000..3d0770e --- /dev/null +++ b/Syrette.Tests/ServiceContainerDisposalTests.cs @@ -0,0 +1,40 @@ +namespace Syrette.Tests; + +public class ServiceContainerDisposalTests +{ + [Fact] + public void Dispose_disposes_singletons_implementing_IDisposable() + { + var container = new ServiceContainer(); + container.AddSingleton(); + var instance = container.GetService(); + + container.Dispose(); + Assert.True(instance.IsDisposed); + } + + [Fact] + public void Dispose_clears_singleton_cache() + { + var container = new ServiceContainer(); + container.AddSingleton(); + container.GetService(); + + container.Dispose(); + + Assert.Throws(() => + container.GetService()); + } + + [Fact] + public void Multiple_dispose_is_safe() + { + var container = new ServiceContainer(); + container.AddSingleton(); + container.GetService(); + + container.Dispose(); + container.Dispose(); + Assert.True(true); + } +} diff --git a/Syrette.Tests/ServiceContainerRegistrationTests.cs b/Syrette.Tests/ServiceContainerRegistrationTests.cs new file mode 100644 index 0000000..e9bbd8a --- /dev/null +++ b/Syrette.Tests/ServiceContainerRegistrationTests.cs @@ -0,0 +1,100 @@ +namespace Syrette.Tests; + +public class ServiceContainerRegistrationTests +{ + [Fact] + public void AddSingleton_TInterface_TImplementation_registers_successfully() + { + var container = new ServiceContainer(); + container.AddSingleton(); + Assert.NotNull(container.GetService()); + } + + [Fact] + public void AddSingleton_TInterface_TImplementation_with_args_registers_successfully() + { + var container = new ServiceContainer(); + container.AddSingleton(Guid.NewGuid()); + Assert.NotNull(container.GetService()); + } + + [Fact] + public void AddSingleton_TClass_registers_successfully() + { + var container = new ServiceContainer(); + container.AddSingleton(); + Assert.NotNull(container.GetService()); + } + + [Fact] + public void AddSingleton_TClass_with_args_registers_successfully() + { + var container = new ServiceContainer(); + container.AddSingleton(Guid.NewGuid()); + Assert.NotNull(container.GetService()); + } + + [Fact] + public void AddTransient_TInterface_TImplementation_registers_successfully() + { + var container = new ServiceContainer(); + container.AddTransient(); + Assert.NotNull(container.GetService()); + } + + [Fact] + public void AddTransient_TInterface_TImplementation_with_args_registers_successfully() + { + var container = new ServiceContainer(); + container.AddTransient(Guid.NewGuid()); + Assert.NotNull(container.GetService()); + } + + [Fact] + public void AddTransient_TClass_registers_successfully() + { + var container = new ServiceContainer(); + container.AddTransient(); + Assert.NotNull(container.GetService()); + } + + [Fact] + public void AddTransient_TClass_with_args_registers_successfully() + { + var container = new ServiceContainer(); + container.AddTransient(Guid.NewGuid()); + Assert.NotNull(container.GetService()); + } + + [Fact] + public void Fluent_chaining_returns_same_container() + { + var container = new ServiceContainer(); + var result = container + .AddSingleton() + .AddTransient(); + Assert.Same(container, result); + } + + [Fact] + public void Duplicate_registration_same_pair_throws() + { + var container = new ServiceContainer(); + container.AddSingleton(); + Assert.Throws(() => + container.AddSingleton()); + } + + [Fact] + public void Duplicate_registration_different_impl_for_same_service_allowed() + { + var container = new ServiceContainer(); + container.AddSingleton(); + container.AddSingleton(); + + var services = container.GetServiceTypes(); + Assert.Equal(2, services.Count); + Assert.Contains(typeof(TestServiceImpl), services); + Assert.Contains(typeof(TestServiceAlt), services); + } +} diff --git a/Syrette.Tests/ServiceContainerResolutionTests.cs b/Syrette.Tests/ServiceContainerResolutionTests.cs new file mode 100644 index 0000000..28e9e59 --- /dev/null +++ b/Syrette.Tests/ServiceContainerResolutionTests.cs @@ -0,0 +1,141 @@ +namespace Syrette.Tests; + +public class ServiceContainerResolutionTests +{ + [Fact] + public void GetService_T_resolves_singleton() + { + var container = new ServiceContainer(); + container.AddSingleton(); + var instance = container.GetService(); + Assert.NotNull(instance); + Assert.IsType(instance); + } + + [Fact] + public void GetService_T_resolves_transient() + { + var container = new ServiceContainer(); + container.AddTransient(); + var instance = container.GetService(); + Assert.NotNull(instance); + Assert.IsType(instance); + } + + [Fact] + public void Singleton_returns_same_instance() + { + var container = new ServiceContainer(); + container.AddSingleton(); + var a = container.GetService(); + var b = container.GetService(); + Assert.Same(a, b); + } + + [Fact] + public void Transient_returns_new_instance() + { + var container = new ServiceContainer(); + container.AddTransient(); + var a = container.GetService(); + var b = container.GetService(); + Assert.NotSame(a, b); + } + + [Fact] + public void GetService_Type_non_generic_resolves() + { + var container = new ServiceContainer(); + container.AddSingleton(); + var instance = container.GetService(typeof(ITestService)); + Assert.NotNull(instance); + Assert.IsType(instance); + } + + [Fact] + public void GetService_Type_with_args_resolves() + { + var container = new ServiceContainer(); + container.AddTransient(); + var instance = container.GetService(typeof(ITestService), new object[] { Guid.NewGuid() }); + Assert.NotNull(instance); + } + + [Fact] + public void GetService_T_throws_for_unregistered() + { + var container = new ServiceContainer(); + Assert.Throws(() => + container.GetService()); + } + + [Fact] + public void GetService_Type_throws_for_unregistered() + { + var container = new ServiceContainer(); + Assert.Throws(() => + container.GetService(typeof(ITestService))); + } + + [Fact] + public void TryGetService_returns_null_for_unregistered() + { + var container = new ServiceContainer(); + var result = container.TryGetService(typeof(ITestService)); + Assert.Null(result); + } + + [Fact] + public void TryGetService_returns_instance_when_registered() + { + var container = new ServiceContainer(); + container.AddSingleton(); + var result = container.TryGetService(typeof(ITestService)); + Assert.NotNull(result); + } + + [Fact] + public void Resolve_by_implementation_type_when_not_registered_directly() + { + var container = new ServiceContainer(); + container.AddSingleton(); + var instance = container.GetService(typeof(TestServiceImpl)); + Assert.NotNull(instance); + } + + [Fact] + public void Resolution_time_args_override_registration_time_args() + { + var container = new ServiceContainer(); + container.AddSingleton(Guid.Empty); + var resolved = container.GetService(typeof(ITestService), new object[] { Guid.NewGuid() }); + Assert.NotNull(resolved); + var deep = (TestDeepService)resolved; + Assert.NotEqual(Guid.Empty, deep.Id); + } + + [Fact] + public void GetServices_returns_all_registered_implementations() + { + var container = new ServiceContainer(); + container.AddSingleton(); + container.AddSingleton(); + var services = container.GetServices(); + Assert.Equal(2, services.Count); + var types = services.Select(s => s.GetType()).ToList(); + Assert.Contains(typeof(TestServiceImpl), types); + Assert.Contains(typeof(TestServiceAlt), types); + } + + [Fact] + public void GetServiceTypes_returns_all_implementation_types() + { + var container = new ServiceContainer(); + container.AddSingleton(); + container.AddSingleton(); + var types = container.GetServiceTypes(); + Assert.Equal(2, types.Count); + Assert.Contains(typeof(TestServiceImpl), types); + Assert.Contains(typeof(TestServiceAlt), types); + } +} diff --git a/Syrette.Tests/ServiceContainerThreadingTests.cs b/Syrette.Tests/ServiceContainerThreadingTests.cs new file mode 100644 index 0000000..39c845c --- /dev/null +++ b/Syrette.Tests/ServiceContainerThreadingTests.cs @@ -0,0 +1,61 @@ +namespace Syrette.Tests; + +public class ServiceContainerThreadingTests +{ + [Fact] + public void Concurrent_singleton_resolution_returns_same_instance() + { + var container = new ServiceContainer(); + container.AddSingleton(); + + var results = new ITestService[10]; + + Parallel.For(0, 10, i => + { + results[i] = container.GetService(); + }); + + for (int i = 1; i < results.Length; i++) + { + Assert.Same(results[0], results[i]); + } + } + + [Fact] + public void Concurrent_transient_resolution_returns_unique_instances() + { + var container = new ServiceContainer(); + container.AddTransient(); + + var results = new ITestService[10]; + + Parallel.For(0, 10, i => + { + results[i] = container.GetService(); + }); + + for (int i = 0; i < results.Length; i++) + { + for (int j = i + 1; j < results.Length; j++) + { + Assert.NotSame(results[i], results[j]); + } + } + } + + [Fact] + public void Concurrent_registration_and_resolution_does_not_crash() + { + var container = new ServiceContainer(); + + Parallel.Invoke( + () => { try { container.AddSingleton(); } catch { } }, + () => { try { container.AddSingleton(); } catch { } }, + () => { try { container.GetService(); } catch { } }, + () => { try { container.AddSingleton(); } catch { } }, + () => { try { container.GetService(); } catch { } } + ); + + Assert.NotNull(container); + } +} diff --git a/Syrette.Tests/Syrette.Tests.csproj b/Syrette.Tests/Syrette.Tests.csproj new file mode 100644 index 0000000..83ef3fa --- /dev/null +++ b/Syrette.Tests/Syrette.Tests.csproj @@ -0,0 +1,25 @@ + + + + enable + enable + false + net10.0;net8.0 + + + + + + + + + + + + + + + + + + diff --git a/Syrette.Tests/TestTypes.cs b/Syrette.Tests/TestTypes.cs new file mode 100644 index 0000000..7d12ad4 --- /dev/null +++ b/Syrette.Tests/TestTypes.cs @@ -0,0 +1,35 @@ +namespace Syrette.Tests; + +public interface ITestService { } +public class TestServiceImpl : ITestService { } +public class TestServiceAlt : ITestService { } + +public interface ITestServiceB { } +public class TestServiceBImpl : ITestServiceB { } + +public interface ITestServiceC { } +public class TestServiceCImpl : ITestServiceC { } + +public class TestMultiCtor +{ + public ITestService? A { get; } + public ITestServiceB? B { get; } + public ITestServiceC? C { get; } + + public TestMultiCtor(ITestService a) { A = a; } + public TestMultiCtor(ITestService a, ITestServiceB b) { A = a; B = b; } + public TestMultiCtor(ITestService a, ITestServiceB b, ITestServiceC c) { A = a; B = b; C = c; } +} + +public class TestDeepService : ITestService +{ + public Guid Id { get; } + + public TestDeepService(Guid id) { Id = id; } +} + +public class TestDisposableService : IDisposable +{ + public bool IsDisposed { get; private set; } + public void Dispose() => IsDisposed = true; +} diff --git a/Syrette.Tests/UnitTest1.cs b/Syrette.Tests/UnitTest1.cs new file mode 100644 index 0000000..80655dd --- /dev/null +++ b/Syrette.Tests/UnitTest1.cs @@ -0,0 +1,10 @@ +namespace Syrette.Tests; + +public class UnitTest1 +{ + [Fact] + public void Test1() + { + + } +} diff --git a/Syrette.sln b/Syrette.sln index d547e9a..0d36704 100644 --- a/Syrette.sln +++ b/Syrette.sln @@ -2,16 +2,44 @@ Microsoft Visual Studio Solution File, Format Version 12.00 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Syrette", "Syrette\Syrette.csproj", "{4730ABA2-3979-4B74-A3FF-042F6C3C47D6}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Syrette.Tests", "Syrette.Tests\Syrette.Tests.csproj", "{CE07A0AF-1EAB-4AFF-B04F-16E0A8A5FC24}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {4730ABA2-3979-4B74-A3FF-042F6C3C47D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {4730ABA2-3979-4B74-A3FF-042F6C3C47D6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4730ABA2-3979-4B74-A3FF-042F6C3C47D6}.Debug|x64.ActiveCfg = Debug|Any CPU + {4730ABA2-3979-4B74-A3FF-042F6C3C47D6}.Debug|x64.Build.0 = Debug|Any CPU + {4730ABA2-3979-4B74-A3FF-042F6C3C47D6}.Debug|x86.ActiveCfg = Debug|Any CPU + {4730ABA2-3979-4B74-A3FF-042F6C3C47D6}.Debug|x86.Build.0 = Debug|Any CPU {4730ABA2-3979-4B74-A3FF-042F6C3C47D6}.Release|Any CPU.ActiveCfg = Release|Any CPU {4730ABA2-3979-4B74-A3FF-042F6C3C47D6}.Release|Any CPU.Build.0 = Release|Any CPU + {4730ABA2-3979-4B74-A3FF-042F6C3C47D6}.Release|x64.ActiveCfg = Release|Any CPU + {4730ABA2-3979-4B74-A3FF-042F6C3C47D6}.Release|x64.Build.0 = Release|Any CPU + {4730ABA2-3979-4B74-A3FF-042F6C3C47D6}.Release|x86.ActiveCfg = Release|Any CPU + {4730ABA2-3979-4B74-A3FF-042F6C3C47D6}.Release|x86.Build.0 = Release|Any CPU + {CE07A0AF-1EAB-4AFF-B04F-16E0A8A5FC24}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CE07A0AF-1EAB-4AFF-B04F-16E0A8A5FC24}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CE07A0AF-1EAB-4AFF-B04F-16E0A8A5FC24}.Debug|x64.ActiveCfg = Debug|Any CPU + {CE07A0AF-1EAB-4AFF-B04F-16E0A8A5FC24}.Debug|x64.Build.0 = Debug|Any CPU + {CE07A0AF-1EAB-4AFF-B04F-16E0A8A5FC24}.Debug|x86.ActiveCfg = Debug|Any CPU + {CE07A0AF-1EAB-4AFF-B04F-16E0A8A5FC24}.Debug|x86.Build.0 = Debug|Any CPU + {CE07A0AF-1EAB-4AFF-B04F-16E0A8A5FC24}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CE07A0AF-1EAB-4AFF-B04F-16E0A8A5FC24}.Release|Any CPU.Build.0 = Release|Any CPU + {CE07A0AF-1EAB-4AFF-B04F-16E0A8A5FC24}.Release|x64.ActiveCfg = Release|Any CPU + {CE07A0AF-1EAB-4AFF-B04F-16E0A8A5FC24}.Release|x64.Build.0 = Release|Any CPU + {CE07A0AF-1EAB-4AFF-B04F-16E0A8A5FC24}.Release|x86.ActiveCfg = Release|Any CPU + {CE07A0AF-1EAB-4AFF-B04F-16E0A8A5FC24}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE EndGlobalSection EndGlobal diff --git a/Syrette/ServiceContainer.cs b/Syrette/ServiceContainer.cs index c9b0d04..8d023bf 100644 --- a/Syrette/ServiceContainer.cs +++ b/Syrette/ServiceContainer.cs @@ -165,12 +165,12 @@ public class ServiceContainer : IDisposable { if (descriptor.Lifetime == ServiceLifetime.Singleton) { lock (singletonLock) { - if (singletons.TryGetValue(descriptor.ServiceType, out var singleton)) { + if (singletons.TryGetValue(descriptor.ImplementationType, out var singleton)) { return singleton; } var instance = Instantiate(descriptor.ImplementationType, bestCtor, mergedArgs); - singletons[descriptor.ServiceType] = instance; + singletons[descriptor.ImplementationType] = instance; return instance; } }