3 Commits

Author SHA1 Message Date
Samuele Lorefice
65f624a355 Exposes GetService(Type, obiect[]? args) and it's safer variant TryGetService() to enable consumers to request a service without needing to do reflection work themselves.
All checks were successful
Nuget Pkg Build / build (push) Successful in 1m11s
bumps version to 0.0.1.8-alpha
2025-10-01 19:03:53 +02:00
Samuele Lorefice
66e7fcc798 Fixes #5 makes copy of the args list instead of stripping it away from the descriptor (preventing catastrophic problems). Bumps version
All checks were successful
Nuget Pkg Build / build (push) Successful in 1m7s
2025-09-24 19:53:29 +02:00
Samuele Lorefice
c888da8045 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
2025-09-24 18:06:57 +02:00
5 changed files with 199 additions and 47 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 class Program {
static void Main(string[] args) { static void Main(string[] args) {
var container = new ServiceContainer() var container = new ServiceContainer()
@@ -63,5 +73,12 @@ static class Program {
container.GetService<GuidDependantService>().LogWithId("Hello, sent from the dependency."); container.GetService<GuidDependantService>().LogWithId("Hello, sent from the dependency.");
container.GetService<IService>().Log("Goodbye, Dependency Injection!"); container.GetService<IService>().Log("Goodbye, Dependency Injection!");
var res = container.GetServices<IService>(); 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

@@ -24,7 +24,7 @@ public class ServiceContainer {
/// <typeparam name="TService"></typeparam> /// <typeparam name="TService"></typeparam>
public List<TService> GetServices<TService>() where TService : class => public List<TService> GetServices<TService>() where TService : class =>
descriptors.Where(d => d.ServiceType == typeof(TService)) descriptors.Where(d => d.ServiceType == typeof(TService))
.Select(d => (TService)GetService(d.ImplementationType)).ToList(); .Select(d => (TService)GetService(d.ImplementationType, d.Arguments?.ToArray())).ToList();
/// <summary> /// <summary>
/// Registers a singleton service with its implementation. /// Registers a singleton service with its implementation.
@@ -34,10 +34,28 @@ public class ServiceContainer {
public ServiceContainer AddSingleton<TInterface, TImplementation>() public ServiceContainer AddSingleton<TInterface, TImplementation>()
where TInterface : class where TInterface : class
where TImplementation : class, TInterface { where TImplementation : class, TInterface {
descriptors.Add(new () { descriptors.Add(new() {
ServiceType = typeof(TInterface), ServiceType = typeof(TInterface),
ImplementationType = typeof(TImplementation), 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; return this;
} }
@@ -48,10 +66,26 @@ public class ServiceContainer {
/// <typeparam name="TClass">Class type of the service</typeparam> /// <typeparam name="TClass">Class type of the service</typeparam>
public ServiceContainer AddSingleton<TClass>() public ServiceContainer AddSingleton<TClass>()
where TClass : class { where TClass : class {
descriptors.Add(new () { descriptors.Add(new() {
ServiceType = typeof(TClass), ServiceType = typeof(TClass),
ImplementationType = 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; return this;
} }
@@ -64,7 +98,7 @@ public class ServiceContainer {
public ServiceContainer AddTransient<TInterface, TImplementation>() public ServiceContainer AddTransient<TInterface, TImplementation>()
where TInterface : class where TInterface : class
where TImplementation : class, TInterface { where TImplementation : class, TInterface {
descriptors.Add(new () { descriptors.Add(new() {
ServiceType = typeof(TInterface), ServiceType = typeof(TInterface),
ImplementationType = typeof(TImplementation), ImplementationType = typeof(TImplementation),
Lifetime = ServiceLifetime.Transient Lifetime = ServiceLifetime.Transient
@@ -72,13 +106,31 @@ public class ServiceContainer {
return this; 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> /// <summary>
/// Registers a transient service where the service type is the same as the implementation type. /// Registers a transient service where the service type is the same as the implementation type.
/// </summary> /// </summary>
/// <typeparam name="TClass">Class type of the service</typeparam> /// <typeparam name="TClass">Class type of the service</typeparam>
public ServiceContainer AddTransient<TClass>() public ServiceContainer AddTransient<TClass>()
where TClass : class { where TClass : class {
descriptors.Add(new () { descriptors.Add(new() {
ServiceType = typeof(TClass), ServiceType = typeof(TClass),
ImplementationType = typeof(TClass), ImplementationType = typeof(TClass),
Lifetime = ServiceLifetime.Transient Lifetime = ServiceLifetime.Transient
@@ -86,15 +138,58 @@ public class ServiceContainer {
return this; return this;
} }
// you can't call generic methods with an unknown type at compile time /// <summary>
// so we use reflection to call the generic GetService<T> method with the provided type /// Registers a transient service where the service type is the same as the implementation type.
// Basically we build the method GetService<serviceType>() at runtime and then call it. /// </summary>
// "Classic black magic sorcery" in reflection. /// <param name="args">Arguments to be passed to the constructor, in order of appearance if of the same type.</param>
private object GetService(Type serviceType) { /// <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;
}
/// <summary>
/// Resolves and returns an instance of the requested service type.
/// </summary>
/// <param name="serviceType">Type of the service that's being requested</param>
/// <param name="args">arguments to pass to the constructor of the service</param>
/// <remarks> 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.</remarks>
/// <returns>An object that is the instantiated service type</returns>
public object GetService(Type serviceType, object[]? args = null) {
List<Type> arguments = [serviceType];
if (args != null) arguments.AddRange(args.ToList().Select(a => a.GetType()));
var method = typeof(ServiceContainer) var method = typeof(ServiceContainer)
.GetMethod(nameof(GetService))! .GetMethod(nameof(GetService))!
.MakeGenericMethod(serviceType); .MakeGenericMethod(arguments.ToArray());
return method.Invoke(this, null)!;
return method.Invoke(this, args)!;
}
/// <summary>
/// tries to resolve and return an instance of the requested service type. Returns null if it fails.
/// </summary>
/// <param name="serviceType">Type of the service that's being requested</param>
/// <param name="args">arguments to pass to the constructor of the service</param>
/// <remarks> 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.</remarks>
/// <returns>An object that is the instantiated service type or null if not found</returns>
public object? TryGetService(Type serviceType, object[]? args = null) {
try {
return GetService(serviceType, args);
} catch {
return null;
}
} }
/// <summary> /// <summary>
@@ -108,25 +203,31 @@ public class ServiceContainer {
if (descriptor == null) throw new Exception($"Service of type {typeof(TService)} not registered."); if (descriptor == null) throw new Exception($"Service of type {typeof(TService)} not registered.");
var ctors = descriptor.ImplementationType.GetConstructors(); var ctors = descriptor.ImplementationType.GetConstructors();
var par = descriptor.Arguments ?? new List<object>();
int max = -1; int max = -1;
ConstructorInfo? bestCtor = null; ConstructorInfo? bestCtor = null;
foreach (var ctor in ctors) { foreach (var ctor in ctors) {
var parameters = ctor.GetParameters(); var parameters = ctor.GetParameters();
//check if all parameters are registered services or optional //check if all parameters are registered services or optional or have been provided as arguments
if (!parameters.All(p => descriptors.Any(d => d.ServiceType == p.ParameterType) || p.IsOptional)) continue; 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 //check if this constructor has more registered parameters than the previous best
int satisfiedParams = parameters.Count(p => descriptors.Any(d => d.ServiceType == p.ParameterType)); 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) { if (satisfiedParams > max) {
max = satisfiedParams; max = satisfiedParams;
bestCtor = ctor; bestCtor = ctor;
} }
} }
if (bestCtor == null) if (bestCtor == null)
throw new Exception($"Cannot create service of type {typeof(TService)}. No suitable constructor found."); throw new Exception($"Cannot create service of type {typeof(TService)}. No suitable constructor found.");
// Transient: create a new instance each time // Transient: create a new instance each time
if (descriptor.Lifetime != ServiceLifetime.Lifetime) { if (descriptor.Lifetime != ServiceLifetime.Singleton) {
var service = Instantiate<TService>(descriptor, bestCtor); var service = Instantiate<TService>(descriptor, bestCtor);
return service; return service;
} }
@@ -135,7 +236,7 @@ public class ServiceContainer {
if (singletons.TryGetValue(descriptor.ServiceType, out object? singleton)) return (TService)singleton; if (singletons.TryGetValue(descriptor.ServiceType, out object? singleton)) return (TService)singleton;
// or create a new one if not yet created. // or create a new one if not yet created.
var newSingleton = Instantiate<TService>(descriptor); var newSingleton = Instantiate<TService>(descriptor, bestCtor);
singletons[descriptor.ServiceType] = newSingleton!; singletons[descriptor.ServiceType] = newSingleton!;
return newSingleton; return newSingleton;
} }
@@ -144,23 +245,44 @@ public class ServiceContainer {
if (ctor == null && descriptor.ImplementationType.GetConstructors().Length > 1) if (ctor == null && descriptor.ImplementationType.GetConstructors().Length > 1)
throw new Exception($"Multiple constructors found for type {descriptor.ImplementationType}. Please provide a specific constructor."); 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 != null ? new List<object>(descriptor.Arguments) : new List<object>();
if (ctor == null) if (ctor == null)
par = descriptor.ImplementationType par = descriptor.ImplementationType
.GetConstructors().Single() .GetConstructors().Single()
.GetParameters() .GetParameters()
.Select(p => p.ParameterType) //.Select(p => p.ParameterType)
.ToList(); .ToList();
else else
par = ctor.GetParameters() par = ctor.GetParameters()
.Select(p => p.ParameterType) //.Select(p => p.ParameterType)
.ToList(); .ToList();
object[] parameters = new object[par.Count]; object[] parameters = new object[par.Count];
for (int i = 0; i < par.Count; i++) for (int i = 0; i < par.Count; i++) {
parameters[i] = GetService(par[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); var service = (TInterface?)Activator.CreateInstance(descriptor.ImplementationType, parameters);

View File

@@ -8,15 +8,28 @@ public class ServiceDescriptor
/// <summary> /// <summary>
/// Gets or sets the type of the service to be provided. /// Gets or sets the type of the service to be provided.
/// </summary> /// </summary>
public required Type ServiceType { get; set; } public required Type ServiceType { get; init; }
/// <summary> /// <summary>
/// Gets or sets the concrete type that implements the service. /// Gets or sets the concrete type that implements the service.
/// </summary> /// </summary>
public required Type ImplementationType { get; set; } public required Type ImplementationType { get; init; }
/// <summary> /// <summary>
/// Gets or sets the lifetime of the service (e.g., Singleton or Transient). /// Gets or sets the lifetime of the service (e.g., Singleton or Transient).
/// </summary> /// </summary>
public required ServiceLifetime Lifetime { get; set; } public required ServiceLifetime Lifetime { get; init; }
/// <summary>
/// Arguments to be passed to the constructor of the implementation type.
/// </summary>
public List<object>? Arguments { get; init; }
/// <summary>
/// Returns a string with the specific type of service, its implementation, and its lifetime.
/// </summary>
/// <returns>{implementation Name} as {Service Name} ({Lifetime})</returns>
public override string ToString() {
return $"{ImplementationType.Name} as {ServiceType.Name} ({Lifetime})";
}
} }

View File

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

View File

@@ -9,7 +9,7 @@
<LangVersion>latest</LangVersion> <LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<Version>0.0.1.5-alpha</Version> <Version>0.0.1.8-alpha</Version>
<Title>Syrette </Title> <Title>Syrette </Title>
<Authors>Lorefice Samuele</Authors> <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> <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>