From 043cba4b3f3293ea0aaef12d6923c3742be489c3 Mon Sep 17 00:00:00 2001 From: Samuele Lorefice Date: Sun, 21 Sep 2025 21:30:41 +0200 Subject: [PATCH] Added support for multiple constructors and best match greedy resolution. --- .gitea/workflows/nuget-pkg-build.yml | 2 +- DISandbox/Program.cs | 17 +++++++- README.md | 2 +- Syrette/ServiceContainer.cs | 61 +++++++++++++++++----------- Syrette/ServiceDescriptor.cs | 5 --- Syrette/Syrette.csproj | 4 +- 6 files changed, 58 insertions(+), 33 deletions(-) diff --git a/.gitea/workflows/nuget-pkg-build.yml b/.gitea/workflows/nuget-pkg-build.yml index 2d56109..6105cb5 100644 --- a/.gitea/workflows/nuget-pkg-build.yml +++ b/.gitea/workflows/nuget-pkg-build.yml @@ -21,6 +21,6 @@ jobs: #- name: Test # run: dotnet test -c Release --no-build - name: Pack nugets - run: dotnet pack Syrette -c Release --no-build --output . + run: dotnet pack Syrette -c Release --no-build --output . --include-symbols --include-source - name: Push to NuGet run: dotnet nuget push "*.nupkg" --api-key ${{secrets.NUGETAPIKEY}} --source https://api.nuget.org/v3/index.json diff --git a/DISandbox/Program.cs b/DISandbox/Program.cs index 9342eea..0ad32fc 100644 --- a/DISandbox/Program.cs +++ b/DISandbox/Program.cs @@ -21,8 +21,23 @@ interface IOtherService { class GuidService : IOtherService { public Guid Id { get; } = Guid.NewGuid(); } +public interface INotRegisteredService { + void DoSomething(); +} + +class GuidDependantService { + private readonly IService logService; + private readonly IOtherService? guidService; + + public GuidDependantService(IService logService, INotRegisteredService guidService) { + this.logService = logService; + } + + public GuidDependantService(IService logService, IOtherService guidService) { + this.logService = logService; + this.guidService = guidService; + } -class GuidDependantService(IService logService, IOtherService guidService) { public void LogWithId(string message) { logService.Log($"[GuidDependantService] {message} (ID: {guidService.Id})"); } diff --git a/README.md b/README.md index d468327..a1f0b4b 100644 --- a/README.md +++ b/README.md @@ -10,11 +10,11 @@ It is designed to be used in minimalistic applications, such as console applicat - **Lightweight**: Minimal codebase with no external dependencies. 1 class, the service container. You don't need anything else. - **Simple API**: You register services using two methods (one for singletons, one for transients) and resolve them with one. - **Supports Singleton and Transient lifetimes**: Choose between singleton (one instance per container) and transient (new instance per resolution) lifetimes for your services. +- **Greedy matching constructor selection**: When resolving a service, the constructor with the most parameters that can be satisfied by the container is chosen. ## Limitations - **No support for scoped lifetimes** - **No support for property injection or method injection.** -- **Every service should have only 1 constructor.** (planned to be lifted in future versions) ## Usage ````csharp diff --git a/Syrette/ServiceContainer.cs b/Syrette/ServiceContainer.cs index eca2483..f499d6d 100644 --- a/Syrette/ServiceContainer.cs +++ b/Syrette/ServiceContainer.cs @@ -1,4 +1,6 @@ -namespace Syrette; +using System.Reflection; + +namespace Syrette; /// /// Container for managing service registrations and resolutions. @@ -26,9 +28,7 @@ public class ServiceContainer { descriptors.Add(new ServiceDescriptor { ServiceType = typeof(TInterface), ImplementationType = typeof(TImplementation), - Lifetime = ServiceLifetime.Lifetime, - RequiredTypes = typeof(TImplementation).GetConstructors().Single() - .GetParameters().Select(p => p.ParameterType).ToList() + Lifetime = ServiceLifetime.Lifetime }); return this; } @@ -43,9 +43,7 @@ public class ServiceContainer { descriptors.Add(new ServiceDescriptor { ServiceType = typeof(TInterface), ImplementationType = typeof(TImplementation), - Lifetime = ServiceLifetime.Transient, - RequiredTypes = typeof(TImplementation).GetConstructors().Single() - .GetParameters().Select(p => p.ParameterType).ToList() + Lifetime = ServiceLifetime.Transient }); return this; } @@ -71,20 +69,27 @@ public class ServiceContainer { if (descriptor == null) throw new Exception($"Service of type {typeof(TInterface)} not registered."); - // Ensure all required dependencies are registered - //TODO: some services might be asking for specific implementations, not interfaces. We should check for that too. - var missing = descriptor.RequiredTypes - //filter all required types that are not in the registered descriptors - .Where(t => descriptors.All(d => d.ServiceType != t)) - .Select(t => t.Name) - .ToList(); + var ctors = descriptor.ImplementationType.GetConstructors(); + int max = -1; + ConstructorInfo? bestCtor = null; - if (missing.Any()) - throw new Exception($"Cannot create service of type {typeof(TInterface)}. Missing dependencies: {string.Join(", ", missing)}"); + foreach (var ctor in ctors) { + var parameters = ctor.GetParameters(); + //check if all parameters are registered services or optional + if (!parameters.All(p => descriptors.Any(d => d.ServiceType == p.ParameterType) || p.IsOptional)) continue; + //check if this constructor has more registered parameters than the previous best + int satisfiedParams = parameters.Count(p => descriptors.Any(d => d.ServiceType == p.ParameterType)); + if (satisfiedParams > max) { + max = satisfiedParams; + bestCtor = ctor; + } + } + if (bestCtor == null) + throw new Exception($"Cannot create service of type {typeof(TInterface)}. No suitable constructor found."); // Transient: create a new instance each time if (descriptor.Lifetime != ServiceLifetime.Lifetime) { - var service = Instantiate(descriptor); + var service = Instantiate(descriptor, bestCtor); return service; } @@ -97,12 +102,22 @@ public class ServiceContainer { return newSingleton; } - private TInterface Instantiate(ServiceDescriptor descriptor) { - var par = descriptor.ImplementationType - .GetConstructors().Single() - .GetParameters() - .Select(p => p.ParameterType) - .ToList(); + private TInterface Instantiate(ServiceDescriptor descriptor, ConstructorInfo? ctor = null) { + if (ctor == null && descriptor.ImplementationType.GetConstructors().Length > 1) + throw new Exception($"Multiple constructors found for type {descriptor.ImplementationType}. Please provide a specific constructor."); + + List par; + + if (ctor == null) + par = descriptor.ImplementationType + .GetConstructors().Single() + .GetParameters() + .Select(p => p.ParameterType) + .ToList(); + else + par = ctor.GetParameters() + .Select(p => p.ParameterType) + .ToList(); object[] parameters = new object[par.Count]; diff --git a/Syrette/ServiceDescriptor.cs b/Syrette/ServiceDescriptor.cs index 587efb6..1b9eec2 100644 --- a/Syrette/ServiceDescriptor.cs +++ b/Syrette/ServiceDescriptor.cs @@ -19,9 +19,4 @@ public class ServiceDescriptor /// Gets or sets the lifetime of the service (e.g., Singleton or Transient). /// public required ServiceLifetime Lifetime { get; set; } - - /// - /// Gets or sets the list of types required by the implementation (dependencies). - /// - public List RequiredTypes { get; set; } = new(); } \ No newline at end of file diff --git a/Syrette/Syrette.csproj b/Syrette/Syrette.csproj index 0f543d0..697dc1f 100644 --- a/Syrette/Syrette.csproj +++ b/Syrette/Syrette.csproj @@ -6,11 +6,10 @@ - net9.0 latest enable enable - 0.0.1.1-alpha + 0.0.1.2-alpha Syrette Lorefice Samuele Syrette is a minimalistic dependency injection library for C#. It aims to provide a simple and efficient way to achieve dependency injections in your applications without the overhead of larger frameworks. @@ -24,6 +23,7 @@ Samuele Lorefice true true + net9.0;net8.0 true