Compare commits
16 Commits
v0.0.1.3-a
...
v0.0.2.1-a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a0e034829 | ||
|
|
b9c4f65892 | ||
|
|
fda62e30bc | ||
|
|
5dfee34bf5 | ||
|
|
8fc2fbc7f1 | ||
|
|
f8f814dd16 | ||
|
|
568ab548e5 | ||
|
|
65f624a355 | ||
|
|
66e7fcc798 | ||
|
|
c888da8045 | ||
|
|
b9fbb4b851 | ||
|
|
3df2f50765 | ||
|
|
d20788de33 | ||
|
|
b8f2ddad5a | ||
| 86513ec6c6 | |||
|
|
debedc837e |
@@ -26,4 +26,6 @@ jobs:
|
||||
- name: Pack nugets
|
||||
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
|
||||
|
||||
14
.idea/.idea.Syrette/.idea/discord.xml
generated
Normal file
14
.idea/.idea.Syrette/.idea/discord.xml
generated
Normal file
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DiscordProjectSettings">
|
||||
<option name="show" value="PROJECT_FILES" />
|
||||
<option name="description" value="" />
|
||||
<option name="applicationTheme" value="default" />
|
||||
<option name="iconsTheme" value="default" />
|
||||
<option name="button1Title" value="" />
|
||||
<option name="button1Url" value="" />
|
||||
<option name="button2Title" value="" />
|
||||
<option name="button2Url" value="" />
|
||||
<option name="customApplicationId" value="" />
|
||||
</component>
|
||||
</project>
|
||||
38
AGENTS.md
Normal file
38
AGENTS.md
Normal file
@@ -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<T>()`, `GetService(Type, args)`, `TryGetService(Type, args)`, `GetServices<T>()`, `GetServiceTypes<T>()`.
|
||||
- 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).
|
||||
@@ -1,14 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Syrette\Syrette.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,59 +0,0 @@
|
||||
using Syrette;
|
||||
|
||||
namespace DISandbox;
|
||||
|
||||
interface IService {
|
||||
public void Log(string message);
|
||||
}
|
||||
|
||||
class Service : IService {
|
||||
public void Log(string message) {
|
||||
Console.WriteLine($"[Service] {message}");
|
||||
}
|
||||
}
|
||||
|
||||
interface IOtherService {
|
||||
public Guid Id { get; }
|
||||
|
||||
public void ShowId() => Console.WriteLine($"[OtherService] ID: {Id}");
|
||||
}
|
||||
|
||||
class GuidService : IOtherService {
|
||||
public Guid Id { get; } = Guid.NewGuid();
|
||||
}
|
||||
public interface INotRegisteredService {
|
||||
void DoSomething();
|
||||
}
|
||||
|
||||
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})");
|
||||
}
|
||||
}
|
||||
|
||||
static class Program {
|
||||
static void Main(string[] args) {
|
||||
var container = new ServiceContainer()
|
||||
.AddSingleton<IService, Service>()
|
||||
.AddTransient<IOtherService, GuidService>()
|
||||
.AddTransient<GuidDependantService, GuidDependantService>();
|
||||
|
||||
var service = container.GetService<IService>();
|
||||
service.Log("Hello, Dependency Injection!");
|
||||
container.GetService<IOtherService>().ShowId();
|
||||
container.GetService<GuidDependantService>().LogWithId("Hello, sent from the dependency.");
|
||||
container.GetService<IService>().Log("Goodbye, Dependency Injection!");
|
||||
}
|
||||
}
|
||||
127
Syrette.Tests/ServiceContainerConstructorSelectionTests.cs
Normal file
127
Syrette.Tests/ServiceContainerConstructorSelectionTests.cs
Normal file
@@ -0,0 +1,127 @@
|
||||
namespace Syrette.Tests;
|
||||
|
||||
public class ServiceContainerConstructorSelectionTests
|
||||
{
|
||||
public class CtorWithOptional
|
||||
{
|
||||
public ITestService? A { get; }
|
||||
public string? Name { get; }
|
||||
|
||||
public CtorWithOptional(ITestService a, string? name = "default")
|
||||
{
|
||||
A = a;
|
||||
Name = name;
|
||||
}
|
||||
}
|
||||
|
||||
public class CtorWithExactMatch
|
||||
{
|
||||
public int Value { get; }
|
||||
|
||||
public CtorWithExactMatch(int value) { Value = value; }
|
||||
public CtorWithExactMatch(int value, string label) { Value = value; }
|
||||
}
|
||||
|
||||
public class NoSatisfiableCtor
|
||||
{
|
||||
public NoSatisfiableCtor(int value) { }
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Greedy constructor picks the ctor with the most parameters satisfiable by registered services")]
|
||||
public void Greedy_picks_most_satisfiable_constructor()
|
||||
{
|
||||
var container = new ServiceContainer();
|
||||
container.AddSingleton<ITestService, TestServiceImpl>();
|
||||
container.AddSingleton<ITestServiceB, TestServiceBImpl>();
|
||||
|
||||
container.AddSingleton<TestMultiCtor>();
|
||||
var instance = container.GetService<TestMultiCtor>();
|
||||
|
||||
Assert.NotNull(instance.A);
|
||||
Assert.NotNull(instance.B);
|
||||
Assert.Null(instance.C);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Greedy constructor picks the ctor with all parameters satisfiable over a partial match")]
|
||||
public void Greedy_picks_all_satisfied_over_partial()
|
||||
{
|
||||
var container = new ServiceContainer();
|
||||
container.AddSingleton<ITestService, TestServiceImpl>();
|
||||
container.AddSingleton<ITestServiceB, TestServiceBImpl>();
|
||||
container.AddSingleton<ITestServiceC, TestServiceCImpl>();
|
||||
|
||||
container.AddSingleton<TestMultiCtor>();
|
||||
var instance = container.GetService<TestMultiCtor>();
|
||||
|
||||
Assert.NotNull(instance.A);
|
||||
Assert.NotNull(instance.B);
|
||||
Assert.NotNull(instance.C);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Greedy constructor falls back to 1-param ctor when only 1 service is available")]
|
||||
public void Greedy_falls_back_to_1_param_when_only_1_satisfiable()
|
||||
{
|
||||
var container = new ServiceContainer();
|
||||
container.AddSingleton<ITestService, TestServiceImpl>();
|
||||
|
||||
container.AddSingleton<TestMultiCtor>();
|
||||
var instance = container.GetService<TestMultiCtor>();
|
||||
|
||||
Assert.NotNull(instance.A);
|
||||
Assert.Null(instance.B);
|
||||
Assert.Null(instance.C);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Optional parameter uses its default value when the parameter type is not registered")]
|
||||
public void Optional_parameter_used_when_not_registered()
|
||||
{
|
||||
var container = new ServiceContainer();
|
||||
container.AddSingleton<ITestService, TestServiceImpl>();
|
||||
container.AddSingleton<CtorWithOptional>();
|
||||
|
||||
var instance = container.GetService<CtorWithOptional>();
|
||||
Assert.NotNull(instance.A);
|
||||
Assert.Equal("default", instance.Name);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Registration-time args satisfy a constructor parameter by type matching")]
|
||||
public void Registration_args_satisfy_constructor()
|
||||
{
|
||||
var container = new ServiceContainer();
|
||||
container.AddSingleton<CtorWithExactMatch, CtorWithExactMatch>(42);
|
||||
|
||||
var instance = container.GetService<CtorWithExactMatch>();
|
||||
Assert.Equal(42, instance.Value);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Resolution-time args satisfy a constructor parameter when no registration-time args exist")]
|
||||
public void Resolution_args_satisfy_constructor()
|
||||
{
|
||||
var container = new ServiceContainer();
|
||||
container.AddSingleton<CtorWithExactMatch, CtorWithExactMatch>();
|
||||
|
||||
var instance = (CtorWithExactMatch)container.GetService(
|
||||
typeof(CtorWithExactMatch), new object[] { 99 });
|
||||
Assert.Equal(99, instance.Value);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Resolution-time args override registration-time args of the same type in constructor selection")]
|
||||
public void Resolution_args_override_registration_args()
|
||||
{
|
||||
var container = new ServiceContainer();
|
||||
container.AddSingleton<CtorWithExactMatch, CtorWithExactMatch>(10);
|
||||
|
||||
var instance = (CtorWithExactMatch)container.GetService(
|
||||
typeof(CtorWithExactMatch), new object[] { 20 });
|
||||
Assert.Equal(20, instance.Value);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Throws InvalidOperationException when no constructor has all required parameters satisfiable")]
|
||||
public void No_suitable_constructor_throws()
|
||||
{
|
||||
var container = new ServiceContainer();
|
||||
container.AddSingleton<NoSatisfiableCtor>();
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
container.GetService<NoSatisfiableCtor>());
|
||||
}
|
||||
}
|
||||
40
Syrette.Tests/ServiceContainerDisposalTests.cs
Normal file
40
Syrette.Tests/ServiceContainerDisposalTests.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
namespace Syrette.Tests;
|
||||
|
||||
public class ServiceContainerDisposalTests
|
||||
{
|
||||
[Fact(DisplayName = "Dispose calls Dispose on singleton instances that implement IDisposable")]
|
||||
public void Dispose_disposes_singletons_implementing_IDisposable()
|
||||
{
|
||||
var container = new ServiceContainer();
|
||||
container.AddSingleton<TestDisposableService>();
|
||||
var instance = container.GetService<TestDisposableService>();
|
||||
|
||||
container.Dispose();
|
||||
Assert.True(instance.IsDisposed);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Dispose clears the singleton cache so subsequent resolution throws")]
|
||||
public void Dispose_clears_singleton_cache()
|
||||
{
|
||||
var container = new ServiceContainer();
|
||||
container.AddSingleton<TestDisposableService>();
|
||||
container.GetService<TestDisposableService>();
|
||||
|
||||
container.Dispose();
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
container.GetService<TestDisposableService>());
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Calling Dispose multiple times does not throw")]
|
||||
public void Multiple_dispose_is_safe()
|
||||
{
|
||||
var container = new ServiceContainer();
|
||||
container.AddSingleton<TestDisposableService>();
|
||||
container.GetService<TestDisposableService>();
|
||||
|
||||
container.Dispose();
|
||||
container.Dispose();
|
||||
Assert.True(true);
|
||||
}
|
||||
}
|
||||
100
Syrette.Tests/ServiceContainerRegistrationTests.cs
Normal file
100
Syrette.Tests/ServiceContainerRegistrationTests.cs
Normal file
@@ -0,0 +1,100 @@
|
||||
namespace Syrette.Tests;
|
||||
|
||||
public class ServiceContainerRegistrationTests
|
||||
{
|
||||
[Fact(DisplayName = "AddSingleton<TInterface, TImplementation> registers a service that can be resolved")]
|
||||
public void AddSingleton_TInterface_TImplementation_registers_successfully()
|
||||
{
|
||||
var container = new ServiceContainer();
|
||||
container.AddSingleton<ITestService, TestServiceImpl>();
|
||||
Assert.NotNull(container.GetService<ITestService>());
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "AddSingleton<TInterface, TImplementation> with constructor args registers and resolves")]
|
||||
public void AddSingleton_TInterface_TImplementation_with_args_registers_successfully()
|
||||
{
|
||||
var container = new ServiceContainer();
|
||||
container.AddSingleton<ITestService, TestDeepService>(Guid.NewGuid());
|
||||
Assert.NotNull(container.GetService<ITestService>());
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "AddSingleton<TClass> (self-registration) registers and resolves")]
|
||||
public void AddSingleton_TClass_registers_successfully()
|
||||
{
|
||||
var container = new ServiceContainer();
|
||||
container.AddSingleton<TestServiceImpl>();
|
||||
Assert.NotNull(container.GetService<TestServiceImpl>());
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "AddSingleton<TClass> with constructor args registers and resolves")]
|
||||
public void AddSingleton_TClass_with_args_registers_successfully()
|
||||
{
|
||||
var container = new ServiceContainer();
|
||||
container.AddSingleton<TestDeepService>(Guid.NewGuid());
|
||||
Assert.NotNull(container.GetService<TestDeepService>());
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "AddTransient<TInterface, TImplementation> registers a service that can be resolved")]
|
||||
public void AddTransient_TInterface_TImplementation_registers_successfully()
|
||||
{
|
||||
var container = new ServiceContainer();
|
||||
container.AddTransient<ITestService, TestServiceImpl>();
|
||||
Assert.NotNull(container.GetService<ITestService>());
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "AddTransient<TInterface, TImplementation> with constructor args registers and resolves")]
|
||||
public void AddTransient_TInterface_TImplementation_with_args_registers_successfully()
|
||||
{
|
||||
var container = new ServiceContainer();
|
||||
container.AddTransient<ITestService, TestDeepService>(Guid.NewGuid());
|
||||
Assert.NotNull(container.GetService<ITestService>());
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "AddTransient<TClass> (self-registration) registers and resolves")]
|
||||
public void AddTransient_TClass_registers_successfully()
|
||||
{
|
||||
var container = new ServiceContainer();
|
||||
container.AddTransient<TestServiceImpl>();
|
||||
Assert.NotNull(container.GetService<TestServiceImpl>());
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "AddTransient<TClass> with constructor args registers and resolves")]
|
||||
public void AddTransient_TClass_with_args_registers_successfully()
|
||||
{
|
||||
var container = new ServiceContainer();
|
||||
container.AddTransient<TestDeepService>(Guid.NewGuid());
|
||||
Assert.NotNull(container.GetService<TestDeepService>());
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Fluent chaining returns the same ServiceContainer instance")]
|
||||
public void Fluent_chaining_returns_same_container()
|
||||
{
|
||||
var container = new ServiceContainer();
|
||||
var result = container
|
||||
.AddSingleton<ITestService, TestServiceImpl>()
|
||||
.AddTransient<TestServiceImpl>();
|
||||
Assert.Same(container, result);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Registering the same (ServiceType, ImplementationType) pair twice throws InvalidOperationException")]
|
||||
public void Duplicate_registration_same_pair_throws()
|
||||
{
|
||||
var container = new ServiceContainer();
|
||||
container.AddSingleton<ITestService, TestServiceImpl>();
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
container.AddSingleton<ITestService, TestServiceImpl>());
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Registering two different implementations for the same service type is allowed and both are resolvable")]
|
||||
public void Duplicate_registration_different_impl_for_same_service_allowed()
|
||||
{
|
||||
var container = new ServiceContainer();
|
||||
container.AddSingleton<ITestService, TestServiceImpl>();
|
||||
container.AddSingleton<ITestService, TestServiceAlt>();
|
||||
|
||||
var services = container.GetServiceTypes<ITestService>();
|
||||
Assert.Equal(2, services.Count);
|
||||
Assert.Contains(typeof(TestServiceImpl), services);
|
||||
Assert.Contains(typeof(TestServiceAlt), services);
|
||||
}
|
||||
}
|
||||
141
Syrette.Tests/ServiceContainerResolutionTests.cs
Normal file
141
Syrette.Tests/ServiceContainerResolutionTests.cs
Normal file
@@ -0,0 +1,141 @@
|
||||
namespace Syrette.Tests;
|
||||
|
||||
public class ServiceContainerResolutionTests
|
||||
{
|
||||
[Fact(DisplayName = "GetService<T> resolves a singleton registration")]
|
||||
public void GetService_T_resolves_singleton()
|
||||
{
|
||||
var container = new ServiceContainer();
|
||||
container.AddSingleton<ITestService, TestServiceImpl>();
|
||||
var instance = container.GetService<ITestService>();
|
||||
Assert.NotNull(instance);
|
||||
Assert.IsType<TestServiceImpl>(instance);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "GetService<T> resolves a transient registration")]
|
||||
public void GetService_T_resolves_transient()
|
||||
{
|
||||
var container = new ServiceContainer();
|
||||
container.AddTransient<ITestServiceB, TestServiceBImpl>();
|
||||
var instance = container.GetService<ITestServiceB>();
|
||||
Assert.NotNull(instance);
|
||||
Assert.IsType<TestServiceBImpl>(instance);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Multiple GetService<T> calls for a singleton return the same instance")]
|
||||
public void Singleton_returns_same_instance()
|
||||
{
|
||||
var container = new ServiceContainer();
|
||||
container.AddSingleton<ITestService, TestServiceImpl>();
|
||||
var a = container.GetService<ITestService>();
|
||||
var b = container.GetService<ITestService>();
|
||||
Assert.Same(a, b);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Multiple GetService<T> calls for a transient return different instances")]
|
||||
public void Transient_returns_new_instance()
|
||||
{
|
||||
var container = new ServiceContainer();
|
||||
container.AddTransient<ITestServiceB, TestServiceBImpl>();
|
||||
var a = container.GetService<ITestServiceB>();
|
||||
var b = container.GetService<ITestServiceB>();
|
||||
Assert.NotSame(a, b);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "GetService(Type) non-generic overload resolves a registered service")]
|
||||
public void GetService_Type_non_generic_resolves()
|
||||
{
|
||||
var container = new ServiceContainer();
|
||||
container.AddSingleton<ITestService, TestServiceImpl>();
|
||||
var instance = container.GetService(typeof(ITestService));
|
||||
Assert.NotNull(instance);
|
||||
Assert.IsType<TestServiceImpl>(instance);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "GetService(Type, object[]) passes resolution-time args to the constructor")]
|
||||
public void GetService_Type_with_args_resolves()
|
||||
{
|
||||
var container = new ServiceContainer();
|
||||
container.AddTransient<ITestService, TestDeepService>();
|
||||
var instance = container.GetService(typeof(ITestService), new object[] { Guid.NewGuid() });
|
||||
Assert.NotNull(instance);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "GetService<T> throws InvalidOperationException when type is not registered")]
|
||||
public void GetService_T_throws_for_unregistered()
|
||||
{
|
||||
var container = new ServiceContainer();
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
container.GetService<ITestService>());
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "GetService(Type) throws InvalidOperationException when type is not registered")]
|
||||
public void GetService_Type_throws_for_unregistered()
|
||||
{
|
||||
var container = new ServiceContainer();
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
container.GetService(typeof(ITestService)));
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "TryGetService(Type) returns null when type is not registered")]
|
||||
public void TryGetService_returns_null_for_unregistered()
|
||||
{
|
||||
var container = new ServiceContainer();
|
||||
var result = container.TryGetService(typeof(ITestService));
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "TryGetService(Type) returns an instance when the type is registered")]
|
||||
public void TryGetService_returns_instance_when_registered()
|
||||
{
|
||||
var container = new ServiceContainer();
|
||||
container.AddSingleton<ITestService, TestServiceImpl>();
|
||||
var result = container.TryGetService(typeof(ITestService));
|
||||
Assert.NotNull(result);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Resolving by implementation type works when only the interface was registered")]
|
||||
public void Resolve_by_implementation_type_when_not_registered_directly()
|
||||
{
|
||||
var container = new ServiceContainer();
|
||||
container.AddSingleton<ITestService, TestServiceImpl>();
|
||||
var instance = container.GetService(typeof(TestServiceImpl));
|
||||
Assert.NotNull(instance);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Resolution-time args override registration-time args of the same type")]
|
||||
public void Resolution_time_args_override_registration_time_args()
|
||||
{
|
||||
var container = new ServiceContainer();
|
||||
container.AddSingleton<ITestService, TestDeepService>(Guid.Empty);
|
||||
var resolved = container.GetService(typeof(ITestService), new object[] { Guid.NewGuid() });
|
||||
Assert.NotNull(resolved);
|
||||
var deep = (TestDeepService)resolved;
|
||||
Assert.NotEqual(Guid.Empty, deep.Id);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "GetServices<T> returns instances of all registered implementations for a service type")]
|
||||
public void GetServices_returns_all_registered_implementations()
|
||||
{
|
||||
var container = new ServiceContainer();
|
||||
container.AddSingleton<ITestService, TestServiceImpl>();
|
||||
container.AddSingleton<ITestService, TestServiceAlt>();
|
||||
var services = container.GetServices<ITestService>();
|
||||
Assert.Equal(2, services.Count);
|
||||
var types = services.Select(s => s.GetType()).ToList();
|
||||
Assert.Contains(typeof(TestServiceImpl), types);
|
||||
Assert.Contains(typeof(TestServiceAlt), types);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "GetServiceTypes<T> returns the implementation types of all registered services")]
|
||||
public void GetServiceTypes_returns_all_implementation_types()
|
||||
{
|
||||
var container = new ServiceContainer();
|
||||
container.AddSingleton<ITestService, TestServiceImpl>();
|
||||
container.AddSingleton<ITestService, TestServiceAlt>();
|
||||
var types = container.GetServiceTypes<ITestService>();
|
||||
Assert.Equal(2, types.Count);
|
||||
Assert.Contains(typeof(TestServiceImpl), types);
|
||||
Assert.Contains(typeof(TestServiceAlt), types);
|
||||
}
|
||||
}
|
||||
61
Syrette.Tests/ServiceContainerThreadingTests.cs
Normal file
61
Syrette.Tests/ServiceContainerThreadingTests.cs
Normal file
@@ -0,0 +1,61 @@
|
||||
namespace Syrette.Tests;
|
||||
|
||||
public class ServiceContainerThreadingTests
|
||||
{
|
||||
[Fact(DisplayName = "Concurrent singleton resolution from multiple threads returns the same instance")]
|
||||
public void Concurrent_singleton_resolution_returns_same_instance()
|
||||
{
|
||||
var container = new ServiceContainer();
|
||||
container.AddSingleton<ITestService, TestServiceImpl>();
|
||||
|
||||
var results = new ITestService[10];
|
||||
|
||||
Parallel.For(0, 10, i =>
|
||||
{
|
||||
results[i] = container.GetService<ITestService>();
|
||||
});
|
||||
|
||||
for (int i = 1; i < results.Length; i++)
|
||||
{
|
||||
Assert.Same(results[0], results[i]);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Concurrent transient resolution from multiple threads returns unique instances")]
|
||||
public void Concurrent_transient_resolution_returns_unique_instances()
|
||||
{
|
||||
var container = new ServiceContainer();
|
||||
container.AddTransient<ITestService, TestServiceImpl>();
|
||||
|
||||
var results = new ITestService[10];
|
||||
|
||||
Parallel.For(0, 10, i =>
|
||||
{
|
||||
results[i] = container.GetService<ITestService>();
|
||||
});
|
||||
|
||||
for (int i = 0; i < results.Length; i++)
|
||||
{
|
||||
for (int j = i + 1; j < results.Length; j++)
|
||||
{
|
||||
Assert.NotSame(results[i], results[j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Concurrent registration and resolution does not crash or deadlock")]
|
||||
public void Concurrent_registration_and_resolution_does_not_crash()
|
||||
{
|
||||
var container = new ServiceContainer();
|
||||
|
||||
Parallel.Invoke(
|
||||
() => { try { container.AddSingleton<ITestService, TestServiceImpl>(); } catch { } },
|
||||
() => { try { container.AddSingleton<ITestService, TestServiceImpl>(); } catch { } },
|
||||
() => { try { container.GetService<ITestService>(); } catch { } },
|
||||
() => { try { container.AddSingleton<ITestService, TestServiceImpl>(); } catch { } },
|
||||
() => { try { container.GetService<ITestService>(); } catch { } }
|
||||
);
|
||||
|
||||
Assert.NotNull(container);
|
||||
}
|
||||
}
|
||||
25
Syrette.Tests/Syrette.Tests.csproj
Normal file
25
Syrette.Tests/Syrette.Tests.csproj
Normal file
@@ -0,0 +1,25 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
<TargetFrameworks>net10.0;net8.0</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.2" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="xunit" Version="2.9.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Syrette\Syrette.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
35
Syrette.Tests/TestTypes.cs
Normal file
35
Syrette.Tests/TestTypes.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
namespace Syrette.Tests;
|
||||
|
||||
public interface ITestService { }
|
||||
public class TestServiceImpl : ITestService { }
|
||||
public class TestServiceAlt : ITestService { }
|
||||
|
||||
public interface ITestServiceB { }
|
||||
public class TestServiceBImpl : ITestServiceB { }
|
||||
|
||||
public interface ITestServiceC { }
|
||||
public class TestServiceCImpl : ITestServiceC { }
|
||||
|
||||
public class TestMultiCtor
|
||||
{
|
||||
public ITestService? A { get; }
|
||||
public ITestServiceB? B { get; }
|
||||
public ITestServiceC? C { get; }
|
||||
|
||||
public TestMultiCtor(ITestService a) { A = a; }
|
||||
public TestMultiCtor(ITestService a, ITestServiceB b) { A = a; B = b; }
|
||||
public TestMultiCtor(ITestService a, ITestServiceB b, ITestServiceC c) { A = a; B = b; C = c; }
|
||||
}
|
||||
|
||||
public class TestDeepService : ITestService
|
||||
{
|
||||
public Guid Id { get; }
|
||||
|
||||
public TestDeepService(Guid id) { Id = id; }
|
||||
}
|
||||
|
||||
public class TestDisposableService : IDisposable
|
||||
{
|
||||
public bool IsDisposed { get; private set; }
|
||||
public void Dispose() => IsDisposed = true;
|
||||
}
|
||||
33
Syrette.sln
33
Syrette.sln
@@ -2,21 +2,44 @@
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Syrette", "Syrette\Syrette.csproj", "{4730ABA2-3979-4B74-A3FF-042F6C3C47D6}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DISandbox", "DISandbox\DISandbox.csproj", "{536F5490-926D-4B2A-8F07-9A7D1F8B9381}"
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Syrette.Tests", "Syrette.Tests\Syrette.Tests.csproj", "{CE07A0AF-1EAB-4AFF-B04F-16E0A8A5FC24}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Debug|x64 = Debug|x64
|
||||
Debug|x86 = Debug|x86
|
||||
Release|Any CPU = Release|Any CPU
|
||||
Release|x64 = Release|x64
|
||||
Release|x86 = Release|x86
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{4730ABA2-3979-4B74-A3FF-042F6C3C47D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{4730ABA2-3979-4B74-A3FF-042F6C3C47D6}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{4730ABA2-3979-4B74-A3FF-042F6C3C47D6}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{4730ABA2-3979-4B74-A3FF-042F6C3C47D6}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{4730ABA2-3979-4B74-A3FF-042F6C3C47D6}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{4730ABA2-3979-4B74-A3FF-042F6C3C47D6}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{4730ABA2-3979-4B74-A3FF-042F6C3C47D6}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{4730ABA2-3979-4B74-A3FF-042F6C3C47D6}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{536F5490-926D-4B2A-8F07-9A7D1F8B9381}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{536F5490-926D-4B2A-8F07-9A7D1F8B9381}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{536F5490-926D-4B2A-8F07-9A7D1F8B9381}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{536F5490-926D-4B2A-8F07-9A7D1F8B9381}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{4730ABA2-3979-4B74-A3FF-042F6C3C47D6}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{4730ABA2-3979-4B74-A3FF-042F6C3C47D6}.Release|x64.Build.0 = Release|Any CPU
|
||||
{4730ABA2-3979-4B74-A3FF-042F6C3C47D6}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{4730ABA2-3979-4B74-A3FF-042F6C3C47D6}.Release|x86.Build.0 = Release|Any CPU
|
||||
{CE07A0AF-1EAB-4AFF-B04F-16E0A8A5FC24}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{CE07A0AF-1EAB-4AFF-B04F-16E0A8A5FC24}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{CE07A0AF-1EAB-4AFF-B04F-16E0A8A5FC24}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{CE07A0AF-1EAB-4AFF-B04F-16E0A8A5FC24}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{CE07A0AF-1EAB-4AFF-B04F-16E0A8A5FC24}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{CE07A0AF-1EAB-4AFF-B04F-16E0A8A5FC24}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{CE07A0AF-1EAB-4AFF-B04F-16E0A8A5FC24}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{CE07A0AF-1EAB-4AFF-B04F-16E0A8A5FC24}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{CE07A0AF-1EAB-4AFF-B04F-16E0A8A5FC24}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{CE07A0AF-1EAB-4AFF-B04F-16E0A8A5FC24}.Release|x64.Build.0 = Release|Any CPU
|
||||
{CE07A0AF-1EAB-4AFF-B04F-16E0A8A5FC24}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{CE07A0AF-1EAB-4AFF-B04F-16E0A8A5FC24}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
||||
@@ -2,130 +2,227 @@
|
||||
|
||||
namespace Syrette;
|
||||
|
||||
/// <summary>
|
||||
/// Container for managing service registrations and resolutions.
|
||||
/// </summary>
|
||||
public class ServiceContainer {
|
||||
public class ServiceContainer : IDisposable {
|
||||
private readonly List<ServiceDescriptor> descriptors = new();
|
||||
private readonly Dictionary<Type, object> singletons = new();
|
||||
private readonly object singletonLock = new();
|
||||
|
||||
/// <summary>
|
||||
/// Get all registered implementation types for a given service type.
|
||||
/// </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>
|
||||
/// 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 List<TService> GetServices<TService>() where TService : class =>
|
||||
descriptors.Where(d => d.ServiceType == typeof(TService))
|
||||
.Select(d => (TService)ResolveService(d.ImplementationType, null)).ToList();
|
||||
|
||||
public ServiceContainer AddSingleton<TInterface, TImplementation>()
|
||||
where TInterface : class
|
||||
where TImplementation : class, TInterface {
|
||||
descriptors.Add(new ServiceDescriptor {
|
||||
ServiceType = typeof(TInterface),
|
||||
ImplementationType = typeof(TImplementation),
|
||||
Lifetime = ServiceLifetime.Lifetime
|
||||
});
|
||||
AddDescriptor(typeof(TInterface), typeof(TImplementation), ServiceLifetime.Singleton, null);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ServiceContainer AddSingleton<TInterface, TImplementation>(params object[] args)
|
||||
where TInterface : class
|
||||
where TImplementation : class, TInterface {
|
||||
AddDescriptor(typeof(TInterface), typeof(TImplementation), ServiceLifetime.Singleton, args);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ServiceContainer AddSingleton<TClass>()
|
||||
where TClass : class {
|
||||
AddDescriptor(typeof(TClass), typeof(TClass), ServiceLifetime.Singleton, null);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ServiceContainer AddSingleton<TImplementation>(params object[] args)
|
||||
where TImplementation : class {
|
||||
AddDescriptor(typeof(TImplementation), typeof(TImplementation), ServiceLifetime.Singleton, args);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a transient 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 AddTransient<TInterface, TImplementation>()
|
||||
where TInterface : class
|
||||
where TImplementation : class, TInterface {
|
||||
descriptors.Add(new ServiceDescriptor {
|
||||
ServiceType = typeof(TInterface),
|
||||
ImplementationType = typeof(TImplementation),
|
||||
Lifetime = ServiceLifetime.Transient
|
||||
});
|
||||
AddDescriptor(typeof(TInterface), typeof(TImplementation), ServiceLifetime.Transient, null);
|
||||
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)!;
|
||||
public ServiceContainer AddTransient<TInterface, TImplementation>(params object[] args)
|
||||
where TInterface : class
|
||||
where TImplementation : class, TInterface {
|
||||
AddDescriptor(typeof(TInterface), typeof(TImplementation), ServiceLifetime.Transient, args);
|
||||
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>
|
||||
/// <returns>Resolved service instance</returns>
|
||||
public TInterface GetService<TInterface>() {
|
||||
var descriptor = descriptors.FirstOrDefault(d => d.ServiceType == typeof(TInterface));
|
||||
public ServiceContainer AddTransient<TClass>()
|
||||
where TClass : class {
|
||||
AddDescriptor(typeof(TClass), typeof(TClass), ServiceLifetime.Transient, null);
|
||||
return this;
|
||||
}
|
||||
|
||||
if (descriptor == null) throw new Exception($"Service of type {typeof(TInterface)} not registered.");
|
||||
public ServiceContainer AddTransient<TClass>(params object[] args)
|
||||
where TClass : class {
|
||||
AddDescriptor(typeof(TClass), typeof(TClass), ServiceLifetime.Transient, args);
|
||||
return this;
|
||||
}
|
||||
|
||||
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()
|
||||
});
|
||||
}
|
||||
|
||||
public object GetService(Type serviceType, object[]? args = null) {
|
||||
return ResolveService(serviceType, args);
|
||||
}
|
||||
|
||||
public TService GetService<TService>() {
|
||||
return (TService)ResolveService(typeof(TService), null);
|
||||
}
|
||||
|
||||
public object? TryGetService(Type serviceType, object[]? args = null) {
|
||||
try {
|
||||
return ResolveService(serviceType, args);
|
||||
}
|
||||
catch (InvalidOperationException) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private object ResolveService(Type serviceType, object[]? resolutionArgs) {
|
||||
var descriptor = descriptors.FirstOrDefault(d =>
|
||||
d.ServiceType == serviceType || d.ImplementationType == serviceType);
|
||||
|
||||
if (descriptor == null) {
|
||||
throw new InvalidOperationException(
|
||||
$"Service of type '{serviceType.Name}' not registered.");
|
||||
}
|
||||
|
||||
var mergedArgs = descriptor.Arguments != null
|
||||
? new List<object>(descriptor.Arguments)
|
||||
: new List<object>();
|
||||
|
||||
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();
|
||||
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
|
||||
if (!parameters.All(p => descriptors.Any(d => d.ServiceType == 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));
|
||||
if (satisfiedParams > max) {
|
||||
max = satisfiedParams;
|
||||
|
||||
if (parameters.Any(p =>
|
||||
descriptors.All(d => d.ServiceType != p.ParameterType) &&
|
||||
mergedArgs.All(a => !p.ParameterType.IsAssignableFrom(a.GetType())) &&
|
||||
!p.IsOptional)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int satisfied = parameters.Count(p =>
|
||||
descriptors.Any(d => d.ServiceType == p.ParameterType));
|
||||
|
||||
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(TInterface)}. No suitable constructor found.");
|
||||
|
||||
// Transient: create a new instance each time
|
||||
if (descriptor.Lifetime != ServiceLifetime.Lifetime) {
|
||||
var service = Instantiate<TInterface>(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 (TInterface)singleton;
|
||||
|
||||
// or create a new one if not yet created.
|
||||
var newSingleton = Instantiate<TInterface>(descriptor);
|
||||
singletons[descriptor.ServiceType] = newSingleton!;
|
||||
return newSingleton;
|
||||
if (descriptor.Lifetime == ServiceLifetime.Singleton) {
|
||||
lock (singletonLock) {
|
||||
if (singletons.TryGetValue(descriptor.ImplementationType, out var singleton)) {
|
||||
return singleton;
|
||||
}
|
||||
|
||||
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.");
|
||||
var instance = Instantiate(descriptor.ImplementationType, bestCtor, mergedArgs);
|
||||
singletons[descriptor.ImplementationType] = instance;
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
|
||||
List<Type> par;
|
||||
return Instantiate(descriptor.ImplementationType, bestCtor, mergedArgs);
|
||||
}
|
||||
|
||||
if (ctor == null)
|
||||
par = descriptor.ImplementationType
|
||||
.GetConstructors().Single()
|
||||
.GetParameters()
|
||||
.Select(p => p.ParameterType)
|
||||
.ToList();
|
||||
else
|
||||
par = ctor.GetParameters()
|
||||
.Select(p => p.ParameterType)
|
||||
.ToList();
|
||||
private object Instantiate(Type implementationType, ConstructorInfo ctor, List<object> args) {
|
||||
var parameters = ctor.GetParameters();
|
||||
var resolvedParams = new object?[parameters.Length];
|
||||
var usedArgs = new List<object>();
|
||||
|
||||
object[] parameters = new object[par.Count];
|
||||
for (var i = 0; i < parameters.Length; i++) {
|
||||
var paramType = parameters[i].ParameterType;
|
||||
|
||||
for (int i = 0; i < par.Count; i++)
|
||||
parameters[i] = GetService(par[i]);
|
||||
var arg = args.FirstOrDefault(a =>
|
||||
!usedArgs.Contains(a) && paramType.IsAssignableFrom(a.GetType()));
|
||||
|
||||
var service = (TInterface?)Activator.CreateInstance(descriptor.ImplementationType, parameters);
|
||||
if (arg != null) {
|
||||
resolvedParams[i] = arg;
|
||||
usedArgs.Add(arg);
|
||||
continue;
|
||||
}
|
||||
|
||||
return service ?? throw new Exception($"Could not create instance of type {descriptor.ImplementationType}");
|
||||
var ctorArg = TryGetService(paramType);
|
||||
if (ctorArg != null) {
|
||||
resolvedParams[i] = ctorArg;
|
||||
continue;
|
||||
}
|
||||
|
||||
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 instance = Activator.CreateInstance(implementationType, resolvedParams);
|
||||
|
||||
return instance ??
|
||||
throw new InvalidOperationException(
|
||||
$"Could not create instance of type '{implementationType.Name}'.");
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
foreach (var disposable in singletons.Values.OfType<IDisposable>()) {
|
||||
disposable.Dispose();
|
||||
}
|
||||
|
||||
singletons.Clear();
|
||||
descriptors.Clear();
|
||||
}
|
||||
}
|
||||
@@ -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.3-alpha</Version>
|
||||
<Version>0.0.2.1-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,7 @@
|
||||
<Company>Samuele Lorefice</Company>
|
||||
<Deterministic>true</Deterministic>
|
||||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
|
||||
<TargetFrameworks>net10.0;net8.0</TargetFrameworks>
|
||||
<EmbedUntrackedSources>true</EmbedUntrackedSources>
|
||||
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
|
||||
</PropertyGroup>
|
||||
|
||||
Reference in New Issue
Block a user