13 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
Samuele Lorefice
b9fbb4b851 Version bump with new fixes
All checks were successful
Nuget Pkg Build / build (push) Successful in 1m0s
2025-09-24 04:08:53 +02:00
Samuele Lorefice
3df2f50765 Fixes #2 by providing a proper implementation of GetServices that actually returns instances of those services instead of just a list of types. 2025-09-24 04:07:24 +02:00
Samuele Lorefice
d20788de33 Rename method GetServices to GetServiceTypes for clarity (relevant for #2) 2025-09-24 03:55:01 +02:00
Samuele Lorefice
b8f2ddad5a Fixes #1 by searching also inside the implementation types list 2025-09-24 03:51:39 +02:00
86513ec6c6 Update .gitea/workflows/nuget-pkg-build.yml 2025-09-22 03:38:32 +02:00
Samuele Lorefice
debedc837e Added shorthands methods for no-interface service types
All checks were successful
Nuget Pkg Build / build (push) Successful in 42s
2025-09-22 03:15:51 +02:00
Samuele Lorefice
d0ccdbfa0f Updated workflow, widened support, now including Net 8, 9 and 10
Some checks failed
Nuget Pkg Build / build (push) Failing after 58s
2025-09-22 02:26:07 +02:00
Samuele Lorefice
16d0142967 enables CI flag 2025-09-21 23:04:36 +02:00
Samuele Lorefice
b06e886cf2 CI/CD Fixes 2025-09-21 21:54:22 +02:00
Samuele Lorefice
043cba4b3f Added support for multiple constructors and best match greedy resolution.
Some checks failed
Nuget Pkg Build / build (push) Failing after 25s
2025-09-21 21:30:41 +02:00
8 changed files with 303 additions and 73 deletions

View File

@@ -11,16 +11,21 @@ jobs:
timeout-minutes: 5
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Setup .NET SDK
uses: actions/setup-dotnet@v4
uses: actions/setup-dotnet@v5
with:
dotnet-version: 9.x
dotnet-version: |
10.x
9.x
8.x
- name: Build
run: dotnet build Syrette -c Release
#- 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 -p:SymbolPackageFormat=snupkg
- name: Push to NuGet
run: dotnet nuget push "*.nupkg" --api-key ${{secrets.NUGETAPIKEY}} --source https://api.nuget.org/v3/index.json
run: dotnet nuget push "*.nupkg" --api-key ${{secrets.NUGETAPIKEY}} --skip-duplicate --source https://api.nuget.org/v3/index.json
- name: Push to Gitea
run: dotnet nuget push "*.nupkg" --api-key ${{secrets.NUGETGITEA}} --skip-duplicate --source https://git.r3d.codes/api/packages/REDCODE/nuget/index.json

View File

@@ -12,6 +12,12 @@ class Service : IService {
}
}
class AnotherService : IService {
public void Log(string message) {
Console.WriteLine($"[AnotherService] {message}");
}
}
interface IOtherService {
public Guid Id { get; }
@@ -21,17 +27,43 @@ interface IOtherService {
class GuidService : IOtherService {
public Guid Id { get; } = Guid.NewGuid();
}
public interface INotRegisteredService {
void DoSomething();
}
class GuidDependantService(IService logService, IOtherService guidService) {
public void LogWithId(string message) {
logService.Log($"[GuidDependantService] {message} (ID: {guidService.Id})");
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;
}
public void LogWithId(string message) {
logService.Log($"[GuidDependantService] {message} (ID: {guidService?.Id})");
}
}
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()
.AddSingleton<IService, Service>()
.AddTransient<IService, AnotherService>()
.AddTransient<IOtherService, GuidService>()
.AddTransient<GuidDependantService, GuidDependantService>();
@@ -40,5 +72,13 @@ static class Program {
container.GetService<IOtherService>().ShowId();
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

@@ -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

View File

@@ -1,4 +1,6 @@
namespace Syrette;
using System.Reflection;
namespace Syrette;
/// <summary>
/// Container for managing service registrations and resolutions.
@@ -12,23 +14,78 @@ public class ServiceContainer {
/// </summary>
/// <typeparam name="TServices"></typeparam>
/// <returns></returns>
public List<Type> GetServices<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>
/// <typeparam name="TService"></typeparam>
public List<TService> GetServices<TService>() where TService : class =>
descriptors.Where(d => d.ServiceType == typeof(TService))
.Select(d => (TService)GetService(d.ImplementationType, d.Arguments?.ToArray())).ToList();
/// <summary>
/// Registers a singleton service with its implementation.
/// </summary>
/// <typeparam name="TInterface">Interface the service is implementing</typeparam>
/// <typeparam name="TImplementation">Implementation type of the service</typeparam>
public ServiceContainer AddSingleton<TInterface, TImplementation>()
where TInterface : class
where TImplementation : class, TInterface {
descriptors.Add(new ServiceDescriptor {
descriptors.Add(new() {
ServiceType = typeof(TInterface),
ImplementationType = typeof(TImplementation),
Lifetime = ServiceLifetime.Lifetime,
RequiredTypes = typeof(TImplementation).GetConstructors().Single()
.GetParameters().Select(p => p.ParameterType).ToList()
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() {
ServiceType = typeof(TClass),
ImplementationType = typeof(TClass),
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;
}
@@ -39,76 +96,194 @@ public class ServiceContainer {
/// <typeparam name="TInterface">Interface the service is implementing</typeparam>
/// <typeparam name="TImplementation">Implementation type of the service</typeparam>
public ServiceContainer AddTransient<TInterface, TImplementation>()
where TInterface : class
where TImplementation : class, TInterface {
descriptors.Add(new ServiceDescriptor {
descriptors.Add(new() {
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;
}
// 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.
// "Classic black magic sorcery" in reflection.
private object GetService(Type serviceType) {
var method = typeof(ServiceContainer)
.GetMethod(nameof(GetService))!
.MakeGenericMethod(serviceType);
return method.Invoke(this, null)!;
/// <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() {
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;
}
/// <summary>
/// Resolves and returns an instance of the requested service type.
/// </summary>
/// <typeparam name="TInterface">Interface type of the service being requested</typeparam>
/// <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)
.GetMethod(nameof(GetService))!
.MakeGenericMethod(arguments.ToArray());
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>
/// Resolves and returns an instance of the requested service type.
/// </summary>
/// <typeparam name="TService">Interface type of the service being requested</typeparam>
/// <returns>Resolved service instance</returns>
public TInterface GetService<TInterface>() {
var descriptor = descriptors.FirstOrDefault(d => d.ServiceType == typeof(TInterface));
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();
if (missing.Any())
throw new Exception($"Cannot create service of type {typeof(TInterface)}. Missing dependencies: {string.Join(", ", missing)}");
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 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) {
var service = Instantiate<TInterface>(descriptor);
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 (TInterface)singleton;
if (singletons.TryGetValue(descriptor.ServiceType, out object? singleton)) return (TService)singleton;
// or create a new one if not yet created.
var newSingleton = Instantiate<TInterface>(descriptor);
var newSingleton = Instantiate<TService>(descriptor, bestCtor);
singletons[descriptor.ServiceType] = newSingleton!;
return newSingleton;
}
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<ParameterInfo> par;
List<object> args = descriptor.Arguments != null ? new List<object>(descriptor.Arguments) : new List<object>();
if (ctor == null)
par = descriptor.ImplementationType
.GetConstructors().Single()
.GetParameters()
//.Select(p => p.ParameterType)
.ToList();
else
par = ctor.GetParameters()
//.Select(p => p.ParameterType)
.ToList();
private TInterface Instantiate<TInterface>(ServiceDescriptor descriptor) {
var par = descriptor.ImplementationType
.GetConstructors().Single()
.GetParameters()
.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

@@ -8,20 +8,28 @@ public class ServiceDescriptor
/// <summary>
/// Gets or sets the type of the service to be provided.
/// </summary>
public required Type ServiceType { get; set; }
public required Type ServiceType { get; init; }
/// <summary>
/// Gets or sets the concrete type that implements the service.
/// </summary>
public required Type ImplementationType { get; set; }
public required Type ImplementationType { get; init; }
/// <summary>
/// Gets or sets the lifetime of the service (e.g., Singleton or Transient).
/// </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>
/// Gets or sets the list of types required by the implementation (dependencies).
/// Returns a string with the specific type of service, its implementation, and its lifetime.
/// </summary>
public List<Type> RequiredTypes { get; set; } = new();
/// <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>
/// 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

@@ -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.8-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,9 @@
<Company>Samuele Lorefice</Company>
<Deterministic>true</Deterministic>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<DebugSymbols>true</DebugSymbols>

View File

@@ -1,7 +1,7 @@
{
"sdk": {
"version": "9.0.0",
"version": "10.0.0",
"rollForward": "latestMinor",
"allowPrerelease": false
"allowPrerelease": true
}
}