Added support for multiple constructors and best match greedy resolution.
Some checks failed
Nuget Pkg Build / build (push) Failing after 25s
Some checks failed
Nuget Pkg Build / build (push) Failing after 25s
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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})");
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
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];
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user