Fixes #4, renames ServiceLifetime.Lifetime to Singleton, adds support for arguments in constructors, version bumps.
All checks were successful
Nuget Pkg Build / build (push) Successful in 49s

This commit is contained in:
Samuele Lorefice
2025-09-24 18:06:57 +02:00
parent b9fbb4b851
commit c888da8045
5 changed files with 161 additions and 33 deletions

View File

@@ -49,6 +49,16 @@ class GuidDependantService {
}
}
public interface ICacheService {
public Uri CacheLocation { get; set; }
}
public class CacheService : ICacheService {
public required Uri CacheLocation { get; set; }
public CacheService() => CacheLocation = new Uri("http://default.cache");
public CacheService(Uri cacheLocation) => CacheLocation = cacheLocation;
}
static class Program {
static void Main(string[] args) {
var container = new ServiceContainer()
@@ -63,5 +73,12 @@ static class Program {
container.GetService<GuidDependantService>().LogWithId("Hello, sent from the dependency.");
container.GetService<IService>().Log("Goodbye, Dependency Injection!");
var res = container.GetServices<IService>();
var testContainer = new ServiceContainer()
.AddSingleton<ICacheService, CacheService>(new Uri("http://cache.local"));
var iCacheService = testContainer.GetService<ICacheService>();
Console.WriteLine($"[ICacheService] {iCacheService.CacheLocation}");
var cacheService = testContainer.GetService<CacheService>();
Console.WriteLine($"[CacheService] {cacheService.CacheLocation}");
}
}

View File

@@ -14,10 +14,10 @@ public class ServiceContainer {
/// </summary>
/// <typeparam name="TServices"></typeparam>
/// <returns></returns>
public List<Type> GetServiceTypes<TServices>() =>
public List<Type> GetServiceTypes<TServices>() =>
descriptors.Where(d => d.ServiceType == typeof(TServices))
.Select(d => d.ImplementationType).ToList();
/// <summary>
/// Get all registered services for a given service type.
/// </summary>
@@ -34,24 +34,58 @@ public class ServiceContainer {
public ServiceContainer AddSingleton<TInterface, TImplementation>()
where TInterface : class
where TImplementation : class, TInterface {
descriptors.Add(new () {
descriptors.Add(new() {
ServiceType = typeof(TInterface),
ImplementationType = typeof(TImplementation),
Lifetime = ServiceLifetime.Lifetime
Lifetime = ServiceLifetime.Singleton
});
return this;
}
/// <summary>
/// Registers a singleton service with its implementation.
/// </summary>
/// <param name="args">Arguments to be passed to the constructor, in order of appearance if of the same type.</param>
/// <typeparam name="TInterface">Interface the service is implementing</typeparam>
/// <typeparam name="TImplementation">Implementation type of the service</typeparam>
public ServiceContainer AddSingleton<TInterface, TImplementation>(params object[] args)
where TInterface : class
where TImplementation : class, TInterface {
descriptors.Add(new() {
ServiceType = typeof(TInterface),
ImplementationType = typeof(TImplementation),
Lifetime = ServiceLifetime.Singleton,
Arguments = args.ToList()
});
return this;
}
/// <summary>
/// Registers a singleton service where the service type is the same as the implementation type.
/// </summary>
/// <typeparam name="TClass">Class type of the service</typeparam>
public ServiceContainer AddSingleton<TClass>()
where TClass : class {
descriptors.Add(new () {
descriptors.Add(new() {
ServiceType = typeof(TClass),
ImplementationType = typeof(TClass),
Lifetime = ServiceLifetime.Lifetime
Lifetime = ServiceLifetime.Singleton
});
return this;
}
/// <summary>
/// Registers a singleton service with its implementation.
/// </summary>
/// <param name="args">Arguments to be passed to the constructor, in order of appearance if of the same type.</param>
/// <typeparam name="TImplementation">Implementation type of the service</typeparam>
public ServiceContainer AddSingleton<TImplementation>(params object[] args)
where TImplementation : class {
descriptors.Add(new() {
ServiceType = typeof(TImplementation),
ImplementationType = typeof(TImplementation),
Lifetime = ServiceLifetime.Singleton,
Arguments = args.ToList()
});
return this;
}
@@ -64,28 +98,62 @@ public class ServiceContainer {
public ServiceContainer AddTransient<TInterface, TImplementation>()
where TInterface : class
where TImplementation : class, TInterface {
descriptors.Add(new () {
descriptors.Add(new() {
ServiceType = typeof(TInterface),
ImplementationType = typeof(TImplementation),
Lifetime = ServiceLifetime.Transient
});
return this;
}
/// <summary>
/// Registers a transient service with its implementation.
/// </summary>
/// <param name="args">Arguments to be passed to the constructor, in order of appearance if of the same type.</param>
/// <typeparam name="TInterface">Interface the service is implementing</typeparam>
/// <typeparam name="TImplementation">Implementation type of the service</typeparam>
public ServiceContainer AddTransient<TInterface, TImplementation>(params object[] args)
where TInterface : class
where TImplementation : class, TInterface {
descriptors.Add(new() {
ServiceType = typeof(TInterface),
ImplementationType = typeof(TImplementation),
Lifetime = ServiceLifetime.Transient,
Arguments = args.ToList()
});
return this;
}
/// <summary>
/// Registers a transient service where the service type is the same as the implementation type.
/// </summary>
/// <typeparam name="TClass">Class type of the service</typeparam>
public ServiceContainer AddTransient<TClass>()
where TClass : class {
descriptors.Add(new () {
descriptors.Add(new() {
ServiceType = typeof(TClass),
ImplementationType = typeof(TClass),
Lifetime = ServiceLifetime.Transient
});
return this;
}
/// <summary>
/// Registers a transient service where the service type is the same as the implementation type.
/// </summary>
/// <param name="args">Arguments to be passed to the constructor, in order of appearance if of the same type.</param>
/// <typeparam name="TClass">Class type of the service</typeparam>
public ServiceContainer AddTransient<TClass>(params object[] args)
where TClass : class {
descriptors.Add(new() {
ServiceType = typeof(TClass),
ImplementationType = typeof(TClass),
Lifetime = ServiceLifetime.Transient,
Arguments = args.ToList()
});
return this;
}
// you can't call generic methods with an unknown type at compile time
// so we use reflection to call the generic GetService<T> method with the provided type
// Basically we build the method GetService<serviceType>() at runtime and then call it.
@@ -97,6 +165,17 @@ public class ServiceContainer {
return method.Invoke(this, null)!;
}
private object? TryGetService(Type serviceType) {
var method = typeof(ServiceContainer)
.GetMethod(nameof(GetService))!
.MakeGenericMethod(serviceType);
try {
return method.Invoke(this, null)!;
} catch {
return null!;
}
}
/// <summary>
/// Resolves and returns an instance of the requested service type.
/// </summary>
@@ -104,38 +183,44 @@ public class ServiceContainer {
/// <returns>Resolved service instance</returns>
public TService GetService<TService>() {
var descriptor = descriptors.FirstOrDefault(d => d.ServiceType == typeof(TService) || d.ImplementationType == typeof(TService));
if (descriptor == null) throw new Exception($"Service of type {typeof(TService)} not registered.");
var ctors = descriptor.ImplementationType.GetConstructors();
var par = descriptor.Arguments ?? new List<object>();
int max = -1;
ConstructorInfo? bestCtor = null;
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 all parameters are registered services or optional or have been provided as arguments
if (parameters.Any(p => descriptors.All(d => d.ServiceType != p.ParameterType) && par.All(a => a.GetType() != 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));
satisfiedParams += par.Count(arg => parameters.Any(p => p.ParameterType == arg.GetType()));
if (satisfiedParams > max) {
max = satisfiedParams;
bestCtor = ctor;
}
}
if (bestCtor == null)
throw new Exception($"Cannot create service of type {typeof(TService)}. No suitable constructor found.");
// Transient: create a new instance each time
if (descriptor.Lifetime != ServiceLifetime.Lifetime) {
if (descriptor.Lifetime != ServiceLifetime.Singleton) {
var service = Instantiate<TService>(descriptor, bestCtor);
return service;
}
// Singleton: return existing instance
if (singletons.TryGetValue(descriptor.ServiceType, out object? singleton)) return (TService)singleton;
// or create a new one if not yet created.
var newSingleton = Instantiate<TService>(descriptor);
var newSingleton = Instantiate<TService>(descriptor, bestCtor);
singletons[descriptor.ServiceType] = newSingleton!;
return newSingleton;
}
@@ -143,25 +228,46 @@ public class ServiceContainer {
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;
List<ParameterInfo> par;
List<object> args = descriptor.Arguments ?? new List<object>();
if (ctor == null)
par = descriptor.ImplementationType
.GetConstructors().Single()
.GetParameters()
.Select(p => p.ParameterType)
//.Select(p => p.ParameterType)
.ToList();
else
else
par = ctor.GetParameters()
.Select(p => p.ParameterType)
//.Select(p => p.ParameterType)
.ToList();
object[] parameters = new object[par.Count];
for (int i = 0; i < par.Count; i++)
parameters[i] = GetService(par[i]);
for (int i = 0; i < par.Count; i++) {
object? arg = args.FirstOrDefault(a => a.GetType() == par[i].ParameterType);
if (arg != null) { // this parameter is satisfied by a provided argument
parameters[i] = arg;
args.Remove(arg); // remove to handle multiple parameters of the same type
continue;
}
arg = TryGetService(par[i].ParameterType);
if (arg != null) {
// this parameter is satisfied by a registered service
parameters[i] = arg;
continue;
}
if (par[i].IsOptional) {
// this parameter is optional and not provided, use default value
parameters[i] = par[i].DefaultValue!;
continue;
}
throw new Exception($"Cannot resolve parameter {par[i].Name} of type {par[i].ParameterType} for service {descriptor.ImplementationType}");
}
var service = (TInterface?)Activator.CreateInstance(descriptor.ImplementationType, parameters);
return service ?? throw new Exception($"Could not create instance of type {descriptor.ImplementationType}");

View File

@@ -19,4 +19,9 @@ public class ServiceDescriptor
/// Gets or sets the lifetime of the service (e.g., Singleton or Transient).
/// </summary>
public required ServiceLifetime Lifetime { get; set; }
/// <summary>
/// Arguments to be passed to the constructor of the implementation type.
/// </summary>
public List<object>? Arguments { get; set; }
}

View File

@@ -7,7 +7,7 @@ public enum ServiceLifetime {
/// <summary>
/// Defines a singleton service, which is created once and shared throughout the application's lifetime.
/// </summary>
Lifetime,
Singleton,
/// <summary>
/// Defines a transient service, which is created anew each time it is requested.
/// </summary>

View File

@@ -9,7 +9,7 @@
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Version>0.0.1.5-alpha</Version>
<Version>0.0.1.6-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>