Compare commits
12 Commits
v0.0.1.2-a
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
65f624a355 | ||
|
|
66e7fcc798 | ||
|
|
c888da8045 | ||
|
|
b9fbb4b851 | ||
|
|
3df2f50765 | ||
|
|
d20788de33 | ||
|
|
b8f2ddad5a | ||
| 86513ec6c6 | |||
|
|
debedc837e | ||
|
|
d0ccdbfa0f | ||
|
|
16d0142967 | ||
|
|
b06e886cf2 |
@@ -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 . --include-symbols --include-source
|
||||
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
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -39,14 +45,25 @@ class GuidDependantService {
|
||||
}
|
||||
|
||||
public void LogWithId(string message) {
|
||||
logService.Log($"[GuidDependantService] {message} (ID: {guidService.Id})");
|
||||
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>();
|
||||
|
||||
@@ -55,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}");
|
||||
}
|
||||
}
|
||||
@@ -14,21 +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
|
||||
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,91 +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
|
||||
});
|
||||
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.");
|
||||
|
||||
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(TInterface)}. 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
|
||||
if (descriptor.Lifetime != ServiceLifetime.Lifetime) {
|
||||
var service = Instantiate<TInterface>(descriptor, bestCtor);
|
||||
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<Type> par;
|
||||
|
||||
|
||||
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)
|
||||
//.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}");
|
||||
|
||||
@@ -8,15 +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>
|
||||
/// 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})";
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<LangVersion>latest</LangVersion>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<Version>0.0.1.2-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>
|
||||
@@ -23,7 +23,9 @@
|
||||
<Company>Samuele Lorefice</Company>
|
||||
<Deterministic>true</Deterministic>
|
||||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||
<TargetFrameworks>net9.0;net8.0</TargetFrameworks>
|
||||
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
|
||||
<EmbedUntrackedSources>true</EmbedUntrackedSources>
|
||||
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"sdk": {
|
||||
"version": "9.0.0",
|
||||
"version": "10.0.0",
|
||||
"rollForward": "latestMinor",
|
||||
"allowPrerelease": false
|
||||
"allowPrerelease": true
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user