Added support for multiple constructors and best match greedy resolution.
Some checks failed
Nuget Pkg Build / build (push) Failing after 25s

This commit is contained in:
Samuele Lorefice
2025-09-21 21:30:41 +02:00
parent e2cc807f70
commit 043cba4b3f
6 changed files with 58 additions and 33 deletions

View File

@@ -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

View File

@@ -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})");
}

View File

@@ -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

View File

@@ -1,4 +1,6 @@
namespace Syrette;
using System.Reflection;
namespace Syrette;
/// <summary>
/// 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<TInterface>(descriptor);
var service = Instantiate<TInterface>(descriptor, bestCtor);
return service;
}
@@ -97,12 +102,22 @@ public class ServiceContainer {
return newSingleton;
}
private TInterface Instantiate<TInterface>(ServiceDescriptor descriptor) {
var par = descriptor.ImplementationType
.GetConstructors().Single()
.GetParameters()
.Select(p => p.ParameterType)
.ToList();
private TInterface Instantiate<TInterface>(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<Type> 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];

View File

@@ -19,9 +19,4 @@ public class ServiceDescriptor
/// Gets or sets the lifetime of the service (e.g., Singleton or Transient).
/// </summary>
public required ServiceLifetime Lifetime { get; set; }
/// <summary>
/// Gets or sets the list of types required by the implementation (dependencies).
/// </summary>
public List<Type> RequiredTypes { get; set; } = new();
}

View File

@@ -6,11 +6,10 @@
</Content>
</ItemGroup>
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Version>0.0.1.1-alpha</Version>
<Version>0.0.1.2-alpha</Version>
<Title>Syrette </Title>
<Authors>Lorefice Samuele</Authors>
<Description>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.</Description>
@@ -24,6 +23,7 @@
<Company>Samuele Lorefice</Company>
<Deterministic>true</Deterministic>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<TargetFrameworks>net9.0;net8.0</TargetFrameworks>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<DebugSymbols>true</DebugSymbols>