diff --git a/DISandbox/Program.cs b/DISandbox/Program.cs index f50aed7..92d9780 100644 --- a/DISandbox/Program.cs +++ b/DISandbox/Program.cs @@ -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().LogWithId("Hello, sent from the dependency."); container.GetService().Log("Goodbye, Dependency Injection!"); var res = container.GetServices(); + + var testContainer = new ServiceContainer() + .AddSingleton(new Uri("http://cache.local")); + var iCacheService = testContainer.GetService(); + Console.WriteLine($"[ICacheService] {iCacheService.CacheLocation}"); + var cacheService = testContainer.GetService(); + Console.WriteLine($"[CacheService] {cacheService.CacheLocation}"); } } \ No newline at end of file diff --git a/Syrette/ServiceContainer.cs b/Syrette/ServiceContainer.cs index a0c226a..06a58e9 100644 --- a/Syrette/ServiceContainer.cs +++ b/Syrette/ServiceContainer.cs @@ -14,10 +14,10 @@ public class ServiceContainer { /// /// /// - public List GetServiceTypes() => + public List GetServiceTypes() => descriptors.Where(d => d.ServiceType == typeof(TServices)) .Select(d => d.ImplementationType).ToList(); - + /// /// Get all registered services for a given service type. /// @@ -34,24 +34,58 @@ public class ServiceContainer { public ServiceContainer AddSingleton() 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; } - + + /// + /// Registers a singleton service with its implementation. + /// + /// Arguments to be passed to the constructor, in order of appearance if of the same type. + /// Interface the service is implementing + /// Implementation type of the service + public ServiceContainer AddSingleton(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; + } + /// /// Registers a singleton service where the service type is the same as the implementation type. /// /// Class type of the service public ServiceContainer AddSingleton() where TClass : class { - descriptors.Add(new () { + descriptors.Add(new() { ServiceType = typeof(TClass), ImplementationType = typeof(TClass), - Lifetime = ServiceLifetime.Lifetime + Lifetime = ServiceLifetime.Singleton + }); + return this; + } + + /// + /// Registers a singleton service with its implementation. + /// + /// Arguments to be passed to the constructor, in order of appearance if of the same type. + /// Implementation type of the service + public ServiceContainer AddSingleton(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() where TInterface : class where TImplementation : class, TInterface { - descriptors.Add(new () { + descriptors.Add(new() { ServiceType = typeof(TInterface), ImplementationType = typeof(TImplementation), Lifetime = ServiceLifetime.Transient }); return this; } - + + /// + /// Registers a transient service with its implementation. + /// + /// Arguments to be passed to the constructor, in order of appearance if of the same type. + /// Interface the service is implementing + /// Implementation type of the service + public ServiceContainer AddTransient(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; + } + /// /// Registers a transient service where the service type is the same as the implementation type. /// /// Class type of the service public ServiceContainer AddTransient() where TClass : class { - descriptors.Add(new () { + descriptors.Add(new() { ServiceType = typeof(TClass), ImplementationType = typeof(TClass), Lifetime = ServiceLifetime.Transient }); return this; } - + + /// + /// Registers a transient service where the service type is the same as the implementation type. + /// + /// Arguments to be passed to the constructor, in order of appearance if of the same type. + /// Class type of the service + public ServiceContainer AddTransient(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 method with the provided type // Basically we build the method GetService() 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!; + } + } + /// /// Resolves and returns an instance of the requested service type. /// @@ -104,38 +183,44 @@ public class ServiceContainer { /// Resolved service instance public TService GetService() { 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(); 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(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(descriptor); + var newSingleton = Instantiate(descriptor, bestCtor); singletons[descriptor.ServiceType] = newSingleton!; return newSingleton; } @@ -143,25 +228,46 @@ public class ServiceContainer { 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; - + + List par; + List args = descriptor.Arguments ?? new List(); + 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}"); diff --git a/Syrette/ServiceDescriptor.cs b/Syrette/ServiceDescriptor.cs index 1b9eec2..4217a9f 100644 --- a/Syrette/ServiceDescriptor.cs +++ b/Syrette/ServiceDescriptor.cs @@ -19,4 +19,9 @@ public class ServiceDescriptor /// Gets or sets the lifetime of the service (e.g., Singleton or Transient). /// public required ServiceLifetime Lifetime { get; set; } + + /// + /// Arguments to be passed to the constructor of the implementation type. + /// + public List? Arguments { get; set; } } \ No newline at end of file diff --git a/Syrette/ServiceLifetime.cs b/Syrette/ServiceLifetime.cs index 1902712..3d16efd 100644 --- a/Syrette/ServiceLifetime.cs +++ b/Syrette/ServiceLifetime.cs @@ -7,7 +7,7 @@ public enum ServiceLifetime { /// /// Defines a singleton service, which is created once and shared throughout the application's lifetime. /// - Lifetime, + Singleton, /// /// Defines a transient service, which is created anew each time it is requested. /// diff --git a/Syrette/Syrette.csproj b/Syrette/Syrette.csproj index 5af542c..7a62022 100644 --- a/Syrette/Syrette.csproj +++ b/Syrette/Syrette.csproj @@ -9,7 +9,7 @@ latest enable enable - 0.0.1.5-alpha + 0.0.1.6-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.