From 568ab548e563873ebded360502d3d0eab1cf3f06 Mon Sep 17 00:00:00 2001 From: Samuele Lorefice Date: Mon, 15 Jun 2026 00:24:44 +0200 Subject: [PATCH] fix: rewrite ServiceContainer resolution logic and add AGENTS.md - Replace broken reflection-based GetService(Type, args) with direct ResolveService method - Fix positional argument matching (type-based FirstOrDefault) - Fix greedy constructor scoring (no double-count) - Narrow TryGetService exception handling to InvalidOperationException - Throw on duplicate registrations - Add IDisposable support - Add thread-safe singleton creation - Create AGENTS.md with repo guide --- AGENTS.md | 38 +++++ Syrette/ServiceContainer.cs | 325 +++++++++++++++--------------------- 2 files changed, 169 insertions(+), 194 deletions(-) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..b58e4e6 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,38 @@ +# Syrette — agent guide + +Minimal C# DI library targeting net8.0;net9.0;net10.0. + +## Build commands + +```bash +dotnet build Syrette # library only +dotnet build # everything (includes DISandbox console app) +dotnet pack Syrette -c Release --output . +``` + +SDK: .NET 10.0+ (see `global.json` — `rollForward: latestMinor`, `allowPrerelease: true`). + +## Project structure + +| Path | Purpose | +|---|---| +| `Syrette/` | Library (class lib). Entrypoints: `ServiceContainer.cs`, `ServiceDescriptor.cs`, `ServiceLifetime.cs` | +| `DISandbox/` | Unofficial manual-test sandbox. Console app referencing `Syrette`. Not part of the shipped package. | + +No test project exists. CI has the test step commented out (`nuget-pkg-build.yml:24`). + +## NuGet publishing + +- CI (Gitea Actions) triggers on `v*` tags or `workflow_dispatch`. +- Pushes to both nuget.org and a Gitea package feed. +- Version is `0.0.1.8-alpha` per `Syrette.csproj`. + +## Library API + +- `ServiceContainer` exposes only `AddSingleton` / `AddTransient` (multiple overloads) and `GetService()`, `GetService(Type, args)`, `TryGetService(Type, args)`, `GetServices()`, `GetServiceTypes()`. +- All registration methods return `ServiceContainer` for fluent chaining. +- Constructor selection is **greedy**: picks the constructor with the most parameters satisfiable by registered services or explicit args. +- Only Singleton and Transient lifetimes; no scoped, no property/method injection. +- `ServiceContainer : IDisposable` — disposes singleton instances that implement `IDisposable`. +- Duplicate registrations (same `(ServiceType, ImplementationType)`) throw `InvalidOperationException`. +- Resolution-time args (`GetService(Type, args)`) override registration-time args of the same type (type-based merge). diff --git a/Syrette/ServiceContainer.cs b/Syrette/ServiceContainer.cs index 39ca0ab..c9b0d04 100644 --- a/Syrette/ServiceContainer.cs +++ b/Syrette/ServiceContainer.cs @@ -2,290 +2,227 @@ namespace Syrette; -/// -/// Container for managing service registrations and resolutions. -/// -public class ServiceContainer { +public class ServiceContainer : IDisposable { private readonly List descriptors = new(); private readonly Dictionary singletons = new(); + private readonly object singletonLock = new(); - /// - /// Get all registered implementation types for a given service type. - /// - /// - /// public List GetServiceTypes() => descriptors.Where(d => d.ServiceType == typeof(TServices)) .Select(d => d.ImplementationType).ToList(); - /// - /// Get all registered services for a given service type. - /// - /// public List GetServices() where TService : class => descriptors.Where(d => d.ServiceType == typeof(TService)) - .Select(d => (TService)GetService(d.ImplementationType, d.Arguments?.ToArray())).ToList(); + .Select(d => (TService)ResolveService(d.ImplementationType, null)).ToList(); - /// - /// Registers a singleton service with its implementation. - /// - /// Interface the service is implementing - /// Implementation type of the service public ServiceContainer AddSingleton() where TInterface : class where TImplementation : class, TInterface { - descriptors.Add(new() { - ServiceType = typeof(TInterface), - ImplementationType = typeof(TImplementation), - Lifetime = ServiceLifetime.Singleton - }); + AddDescriptor(typeof(TInterface), typeof(TImplementation), ServiceLifetime.Singleton, null); 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() - }); + AddDescriptor(typeof(TInterface), typeof(TImplementation), ServiceLifetime.Singleton, args); 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() { - ServiceType = typeof(TClass), - ImplementationType = typeof(TClass), - Lifetime = ServiceLifetime.Singleton - }); + AddDescriptor(typeof(TClass), typeof(TClass), ServiceLifetime.Singleton, null); 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() - }); + AddDescriptor(typeof(TImplementation), typeof(TImplementation), ServiceLifetime.Singleton, args); return this; } - /// - /// Registers a transient service with its implementation. - /// - /// Interface the service is implementing - /// Implementation type of the service public ServiceContainer AddTransient() where TInterface : class where TImplementation : class, TInterface { - descriptors.Add(new() { - ServiceType = typeof(TInterface), - ImplementationType = typeof(TImplementation), - Lifetime = ServiceLifetime.Transient - }); + AddDescriptor(typeof(TInterface), typeof(TImplementation), ServiceLifetime.Transient, null); 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() - }); + AddDescriptor(typeof(TInterface), typeof(TImplementation), ServiceLifetime.Transient, args); 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() { - ServiceType = typeof(TClass), - ImplementationType = typeof(TClass), - Lifetime = ServiceLifetime.Transient - }); + AddDescriptor(typeof(TClass), typeof(TClass), ServiceLifetime.Transient, null); 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() - }); + AddDescriptor(typeof(TClass), typeof(TClass), ServiceLifetime.Transient, args); return this; } - - /// - /// Resolves and returns an instance of the requested service type. - /// - /// Type of the service that's being requested - /// arguments to pass to the constructor of the service - /// 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. - /// An object that is the instantiated service type - public object GetService(Type serviceType, object[]? args = null) { - List arguments = [serviceType]; - if (args != null) arguments.AddRange(args.ToList().Select(a => a.GetType())); - - var method = typeof(ServiceContainer) - .GetMethod(nameof(GetService))! - .MakeGenericMethod(arguments.ToArray()); - - return method.Invoke(this, args)!; + private void AddDescriptor(Type serviceType, Type implementationType, ServiceLifetime lifetime, object[]? args) { + if (descriptors.Any(d => d.ServiceType == serviceType && d.ImplementationType == implementationType)) { + throw new InvalidOperationException( + $"A registration for '{implementationType.Name}' as '{serviceType.Name}' already exists."); + } + + descriptors.Add(new ServiceDescriptor { + ServiceType = serviceType, + ImplementationType = implementationType, + Lifetime = lifetime, + Arguments = args?.ToList() + }); } - - /// - /// tries to resolve and return an instance of the requested service type. Returns null if it fails. - /// - /// Type of the service that's being requested - /// arguments to pass to the constructor of the service - /// 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. - /// An object that is the instantiated service type or null if not found + + public object GetService(Type serviceType, object[]? args = null) { + return ResolveService(serviceType, args); + } + + public TService GetService() { + return (TService)ResolveService(typeof(TService), null); + } + public object? TryGetService(Type serviceType, object[]? args = null) { try { - return GetService(serviceType, args); - } catch { + return ResolveService(serviceType, args); + } + catch (InvalidOperationException) { return null; } } - /// - /// Resolves and returns an instance of the requested service type. - /// - /// Interface type of the service being requested - /// Resolved service instance - public TService GetService() { - var descriptor = descriptors.FirstOrDefault(d => d.ServiceType == typeof(TService) || d.ImplementationType == typeof(TService)); + private object ResolveService(Type serviceType, object[]? resolutionArgs) { + var descriptor = descriptors.FirstOrDefault(d => + d.ServiceType == serviceType || d.ImplementationType == serviceType); - if (descriptor == null) throw new Exception($"Service of type {typeof(TService)} not registered."); + if (descriptor == null) { + throw new InvalidOperationException( + $"Service of type '{serviceType.Name}' not registered."); + } + + var mergedArgs = descriptor.Arguments != null + ? new List(descriptor.Arguments) + : new List(); + + if (resolutionArgs != null) { + foreach (var arg in resolutionArgs) { + var argType = arg.GetType(); + var index = mergedArgs.FindIndex(a => a.GetType() == argType); + + if (index >= 0) { + mergedArgs[index] = arg; + } else { + mergedArgs.Add(arg); + } + } + } var ctors = descriptor.ImplementationType.GetConstructors(); - var par = descriptor.Arguments ?? new List(); - int max = -1; ConstructorInfo? bestCtor = null; + int max = -1; foreach (var ctor in ctors) { var parameters = ctor.GetParameters(); - //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)) + + if (parameters.Any(p => + descriptors.All(d => d.ServiceType != p.ParameterType) && + mergedArgs.All(a => !p.ParameterType.IsAssignableFrom(a.GetType())) && + !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())); + int satisfied = parameters.Count(p => + descriptors.Any(d => d.ServiceType == p.ParameterType)); - if (satisfiedParams > max) { - max = satisfiedParams; + int argSatisfied = 0; + + foreach (var param in parameters) { + if (!descriptors.Any(d => d.ServiceType == param.ParameterType) && + mergedArgs.Any(a => param.ParameterType.IsAssignableFrom(a.GetType()))) { + argSatisfied++; + } + } + + satisfied += argSatisfied; + + if (satisfied > max) { + max = satisfied; 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.Singleton) { - var service = Instantiate(descriptor, bestCtor); - return service; + if (bestCtor == null) { + throw new InvalidOperationException( + $"Cannot create service of type '{serviceType.Name}'. No suitable constructor found."); } - // Singleton: return existing instance - if (singletons.TryGetValue(descriptor.ServiceType, out object? singleton)) return (TService)singleton; + if (descriptor.Lifetime == ServiceLifetime.Singleton) { + lock (singletonLock) { + if (singletons.TryGetValue(descriptor.ServiceType, out var singleton)) { + return singleton; + } - // or create a new one if not yet created. - var newSingleton = Instantiate(descriptor, bestCtor); - singletons[descriptor.ServiceType] = newSingleton!; - return newSingleton; + var instance = Instantiate(descriptor.ImplementationType, bestCtor, mergedArgs); + singletons[descriptor.ServiceType] = instance; + return instance; + } + } + + return Instantiate(descriptor.ImplementationType, bestCtor, mergedArgs); } - - 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 args = descriptor.Arguments != null ? new List(descriptor.Arguments) : new List(); + private object Instantiate(Type implementationType, ConstructorInfo ctor, List args) { + var parameters = ctor.GetParameters(); + var resolvedParams = new object?[parameters.Length]; + var usedArgs = new List(); - if (ctor == null) - par = descriptor.ImplementationType - .GetConstructors().Single() - .GetParameters() - //.Select(p => p.ParameterType) - .ToList(); - else - par = ctor.GetParameters() - //.Select(p => p.ParameterType) - .ToList(); + for (var i = 0; i < parameters.Length; i++) { + var paramType = parameters[i].ParameterType; - object[] parameters = new object[par.Count]; + var arg = args.FirstOrDefault(a => + !usedArgs.Contains(a) && paramType.IsAssignableFrom(a.GetType())); - 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; + resolvedParams[i] = arg; + usedArgs.Add(arg); continue; } - - if (par[i].IsOptional) { - // this parameter is optional and not provided, use default value - parameters[i] = par[i].DefaultValue!; + + var ctorArg = TryGetService(paramType); + if (ctorArg != null) { + resolvedParams[i] = ctorArg; continue; } - throw new Exception($"Cannot resolve parameter {par[i].Name} of type {par[i].ParameterType} for service {descriptor.ImplementationType}"); + + if (parameters[i].IsOptional) { + resolvedParams[i] = parameters[i].DefaultValue; + continue; + } + + throw new InvalidOperationException( + $"Cannot resolve parameter '{parameters[i].Name}' of type '{paramType.Name}' for service '{implementationType.Name}'."); } - var service = (TInterface?)Activator.CreateInstance(descriptor.ImplementationType, parameters); + var instance = Activator.CreateInstance(implementationType, resolvedParams); - return service ?? throw new Exception($"Could not create instance of type {descriptor.ImplementationType}"); + return instance ?? + throw new InvalidOperationException( + $"Could not create instance of type '{implementationType.Name}'."); + } + + public void Dispose() { + foreach (var disposable in singletons.Values.OfType()) { + disposable.Dispose(); + } + + singletons.Clear(); + descriptors.Clear(); } } \ No newline at end of file