Compare commits
9 Commits
v0.0.1.6-a
...
v0.0.2.1-a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a0e034829 | ||
|
|
b9c4f65892 | ||
|
|
fda62e30bc | ||
|
|
5dfee34bf5 | ||
|
|
8fc2fbc7f1 | ||
|
|
f8f814dd16 | ||
|
|
568ab548e5 | ||
|
|
65f624a355 | ||
|
|
66e7fcc798 |
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,84 +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}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class AnotherService : IService {
|
|
||||||
public void Log(string message) {
|
|
||||||
Console.WriteLine($"[AnotherService] {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})");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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>();
|
|
||||||
|
|
||||||
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!");
|
|
||||||
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}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Syrette", "Syrette\Syrette.csproj", "{4730ABA2-3979-4B74-A3FF-042F6C3C47D6}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Syrette", "Syrette\Syrette.csproj", "{4730ABA2-3979-4B74-A3FF-042F6C3C47D6}"
|
||||||
EndProject
|
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
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Debug|x64 = Debug|x64
|
||||||
|
Debug|x86 = Debug|x86
|
||||||
Release|Any CPU = Release|Any CPU
|
Release|Any CPU = Release|Any CPU
|
||||||
|
Release|x64 = Release|x64
|
||||||
|
Release|x86 = Release|x86
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
{4730ABA2-3979-4B74-A3FF-042F6C3C47D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{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|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.ActiveCfg = Release|Any CPU
|
||||||
{4730ABA2-3979-4B74-A3FF-042F6C3C47D6}.Release|Any CPU.Build.0 = 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
|
{4730ABA2-3979-4B74-A3FF-042F6C3C47D6}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
{536F5490-926D-4B2A-8F07-9A7D1F8B9381}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{4730ABA2-3979-4B74-A3FF-042F6C3C47D6}.Release|x64.Build.0 = Release|Any CPU
|
||||||
{536F5490-926D-4B2A-8F07-9A7D1F8B9381}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{4730ABA2-3979-4B74-A3FF-042F6C3C47D6}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
{536F5490-926D-4B2A-8F07-9A7D1F8B9381}.Release|Any CPU.Build.0 = 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
|
EndGlobalSection
|
||||||
EndGlobal
|
EndGlobal
|
||||||
|
|||||||
@@ -2,274 +2,227 @@
|
|||||||
|
|
||||||
namespace Syrette;
|
namespace Syrette;
|
||||||
|
|
||||||
/// <summary>
|
public class ServiceContainer : IDisposable {
|
||||||
/// Container for managing service registrations and resolutions.
|
|
||||||
/// </summary>
|
|
||||||
public class ServiceContainer {
|
|
||||||
private readonly List<ServiceDescriptor> descriptors = new();
|
private readonly List<ServiceDescriptor> descriptors = new();
|
||||||
private readonly Dictionary<Type, object> singletons = 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> GetServiceTypes<TServices>() =>
|
public List<Type> GetServiceTypes<TServices>() =>
|
||||||
descriptors.Where(d => d.ServiceType == typeof(TServices))
|
descriptors.Where(d => d.ServiceType == typeof(TServices))
|
||||||
.Select(d => d.ImplementationType).ToList();
|
.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 =>
|
public List<TService> GetServices<TService>() where TService : class =>
|
||||||
descriptors.Where(d => d.ServiceType == typeof(TService))
|
descriptors.Where(d => d.ServiceType == typeof(TService))
|
||||||
.Select(d => (TService)GetService(d.ImplementationType)).ToList();
|
.Select(d => (TService)ResolveService(d.ImplementationType, null)).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>()
|
public ServiceContainer AddSingleton<TInterface, TImplementation>()
|
||||||
where TInterface : class
|
where TInterface : class
|
||||||
where TImplementation : class, TInterface {
|
where TImplementation : class, TInterface {
|
||||||
descriptors.Add(new() {
|
AddDescriptor(typeof(TInterface), typeof(TImplementation), ServiceLifetime.Singleton, null);
|
||||||
ServiceType = typeof(TInterface),
|
|
||||||
ImplementationType = typeof(TImplementation),
|
|
||||||
Lifetime = ServiceLifetime.Singleton
|
|
||||||
});
|
|
||||||
return this;
|
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)
|
public ServiceContainer AddSingleton<TInterface, TImplementation>(params object[] args)
|
||||||
where TInterface : class
|
where TInterface : class
|
||||||
where TImplementation : class, TInterface {
|
where TImplementation : class, TInterface {
|
||||||
descriptors.Add(new() {
|
AddDescriptor(typeof(TInterface), typeof(TImplementation), ServiceLifetime.Singleton, args);
|
||||||
ServiceType = typeof(TInterface),
|
|
||||||
ImplementationType = typeof(TImplementation),
|
|
||||||
Lifetime = ServiceLifetime.Singleton,
|
|
||||||
Arguments = args.ToList()
|
|
||||||
});
|
|
||||||
return this;
|
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>()
|
public ServiceContainer AddSingleton<TClass>()
|
||||||
where TClass : class {
|
where TClass : class {
|
||||||
descriptors.Add(new() {
|
AddDescriptor(typeof(TClass), typeof(TClass), ServiceLifetime.Singleton, null);
|
||||||
ServiceType = typeof(TClass),
|
|
||||||
ImplementationType = typeof(TClass),
|
|
||||||
Lifetime = ServiceLifetime.Singleton
|
|
||||||
});
|
|
||||||
return this;
|
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)
|
public ServiceContainer AddSingleton<TImplementation>(params object[] args)
|
||||||
where TImplementation : class {
|
where TImplementation : class {
|
||||||
descriptors.Add(new() {
|
AddDescriptor(typeof(TImplementation), typeof(TImplementation), ServiceLifetime.Singleton, args);
|
||||||
ServiceType = typeof(TImplementation),
|
|
||||||
ImplementationType = typeof(TImplementation),
|
|
||||||
Lifetime = ServiceLifetime.Singleton,
|
|
||||||
Arguments = args.ToList()
|
|
||||||
});
|
|
||||||
return this;
|
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>()
|
public ServiceContainer AddTransient<TInterface, TImplementation>()
|
||||||
where TInterface : class
|
where TInterface : class
|
||||||
where TImplementation : class, TInterface {
|
where TImplementation : class, TInterface {
|
||||||
descriptors.Add(new() {
|
AddDescriptor(typeof(TInterface), typeof(TImplementation), ServiceLifetime.Transient, null);
|
||||||
ServiceType = typeof(TInterface),
|
|
||||||
ImplementationType = typeof(TImplementation),
|
|
||||||
Lifetime = ServiceLifetime.Transient
|
|
||||||
});
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <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)
|
public ServiceContainer AddTransient<TInterface, TImplementation>(params object[] args)
|
||||||
where TInterface : class
|
where TInterface : class
|
||||||
where TImplementation : class, TInterface {
|
where TImplementation : class, TInterface {
|
||||||
descriptors.Add(new() {
|
AddDescriptor(typeof(TInterface), typeof(TImplementation), ServiceLifetime.Transient, args);
|
||||||
ServiceType = typeof(TInterface),
|
|
||||||
ImplementationType = typeof(TImplementation),
|
|
||||||
Lifetime = ServiceLifetime.Transient,
|
|
||||||
Arguments = args.ToList()
|
|
||||||
});
|
|
||||||
return this;
|
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>()
|
public ServiceContainer AddTransient<TClass>()
|
||||||
where TClass : class {
|
where TClass : class {
|
||||||
descriptors.Add(new() {
|
AddDescriptor(typeof(TClass), typeof(TClass), ServiceLifetime.Transient, null);
|
||||||
ServiceType = typeof(TClass),
|
|
||||||
ImplementationType = typeof(TClass),
|
|
||||||
Lifetime = ServiceLifetime.Transient
|
|
||||||
});
|
|
||||||
return this;
|
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)
|
public ServiceContainer AddTransient<TClass>(params object[] args)
|
||||||
where TClass : class {
|
where TClass : class {
|
||||||
descriptors.Add(new() {
|
AddDescriptor(typeof(TClass), typeof(TClass), ServiceLifetime.Transient, args);
|
||||||
ServiceType = typeof(TClass),
|
|
||||||
ImplementationType = typeof(TClass),
|
|
||||||
Lifetime = ServiceLifetime.Transient,
|
|
||||||
Arguments = args.ToList()
|
|
||||||
});
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
// you can't call generic methods with an unknown type at compile time
|
private void AddDescriptor(Type serviceType, Type implementationType, ServiceLifetime lifetime, object[]? args) {
|
||||||
// so we use reflection to call the generic GetService<T> method with the provided type
|
if (descriptors.Any(d => d.ServiceType == serviceType && d.ImplementationType == implementationType)) {
|
||||||
// Basically we build the method GetService<serviceType>() at runtime and then call it.
|
throw new InvalidOperationException(
|
||||||
// "Classic black magic sorcery" in reflection.
|
$"A registration for '{implementationType.Name}' as '{serviceType.Name}' already exists.");
|
||||||
private object GetService(Type serviceType) {
|
|
||||||
var method = typeof(ServiceContainer)
|
|
||||||
.GetMethod(nameof(GetService))!
|
|
||||||
.MakeGenericMethod(serviceType);
|
|
||||||
return method.Invoke(this, null)!;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private object? TryGetService(Type serviceType) {
|
descriptors.Add(new ServiceDescriptor {
|
||||||
var method = typeof(ServiceContainer)
|
ServiceType = serviceType,
|
||||||
.GetMethod(nameof(GetService))!
|
ImplementationType = implementationType,
|
||||||
.MakeGenericMethod(serviceType);
|
Lifetime = lifetime,
|
||||||
try {
|
Arguments = args?.ToList()
|
||||||
return method.Invoke(this, null)!;
|
});
|
||||||
} catch {
|
|
||||||
return null!;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public object GetService(Type serviceType, object[]? args = null) {
|
||||||
|
return ResolveService(serviceType, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <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 TService GetService<TService>() {
|
public TService GetService<TService>() {
|
||||||
var descriptor = descriptors.FirstOrDefault(d => d.ServiceType == typeof(TService) || d.ImplementationType == typeof(TService));
|
return (TService)ResolveService(typeof(TService), null);
|
||||||
|
}
|
||||||
|
|
||||||
if (descriptor == null) throw new Exception($"Service of type {typeof(TService)} not registered.");
|
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();
|
var ctors = descriptor.ImplementationType.GetConstructors();
|
||||||
var par = descriptor.Arguments ?? new List<object>();
|
|
||||||
int max = -1;
|
|
||||||
ConstructorInfo? bestCtor = null;
|
ConstructorInfo? bestCtor = null;
|
||||||
|
int max = -1;
|
||||||
|
|
||||||
foreach (var ctor in ctors) {
|
foreach (var ctor in ctors) {
|
||||||
var parameters = ctor.GetParameters();
|
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;
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
//check if this constructor has more registered parameters than the previous best
|
int satisfied = parameters.Count(p =>
|
||||||
int satisfiedParams = parameters.Count(p => descriptors.Any(d => d.ServiceType == p.ParameterType));
|
descriptors.Any(d => d.ServiceType == p.ParameterType));
|
||||||
satisfiedParams += par.Count(arg => parameters.Any(p => p.ParameterType == arg.GetType()));
|
|
||||||
|
|
||||||
if (satisfiedParams > max) {
|
int argSatisfied = 0;
|
||||||
max = satisfiedParams;
|
|
||||||
|
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;
|
bestCtor = ctor;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bestCtor == null)
|
if (bestCtor == null) {
|
||||||
throw new Exception($"Cannot create service of type {typeof(TService)}. No suitable constructor found.");
|
throw new InvalidOperationException(
|
||||||
|
$"Cannot create service of type '{serviceType.Name}'. No suitable constructor found.");
|
||||||
// Transient: create a new instance each time
|
|
||||||
if (descriptor.Lifetime != ServiceLifetime.Singleton) {
|
|
||||||
var service = Instantiate<TService>(descriptor, bestCtor);
|
|
||||||
return service;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Singleton: return existing instance
|
if (descriptor.Lifetime == ServiceLifetime.Singleton) {
|
||||||
if (singletons.TryGetValue(descriptor.ServiceType, out object? singleton)) return (TService)singleton;
|
lock (singletonLock) {
|
||||||
|
if (singletons.TryGetValue(descriptor.ImplementationType, out var singleton)) {
|
||||||
// or create a new one if not yet created.
|
return singleton;
|
||||||
var newSingleton = Instantiate<TService>(descriptor, bestCtor);
|
|
||||||
singletons[descriptor.ServiceType] = newSingleton!;
|
|
||||||
return newSingleton;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private TInterface Instantiate<TInterface>(ServiceDescriptor descriptor, ConstructorInfo? ctor = null) {
|
var instance = Instantiate(descriptor.ImplementationType, bestCtor, mergedArgs);
|
||||||
if (ctor == null && descriptor.ImplementationType.GetConstructors().Length > 1)
|
singletons[descriptor.ImplementationType] = instance;
|
||||||
throw new Exception($"Multiple constructors found for type {descriptor.ImplementationType}. Please provide a specific constructor.");
|
return instance;
|
||||||
|
}
|
||||||
List<ParameterInfo> par;
|
|
||||||
List<object> args = 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();
|
|
||||||
|
|
||||||
object[] parameters = new object[par.Count];
|
|
||||||
|
|
||||||
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);
|
return Instantiate(descriptor.ImplementationType, bestCtor, mergedArgs);
|
||||||
|
}
|
||||||
|
|
||||||
|
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>();
|
||||||
|
|
||||||
|
for (var i = 0; i < parameters.Length; i++) {
|
||||||
|
var paramType = parameters[i].ParameterType;
|
||||||
|
|
||||||
|
var arg = args.FirstOrDefault(a =>
|
||||||
|
!usedArgs.Contains(a) && paramType.IsAssignableFrom(a.GetType()));
|
||||||
|
|
||||||
if (arg != null) {
|
if (arg != null) {
|
||||||
// this parameter is satisfied by a registered service
|
resolvedParams[i] = arg;
|
||||||
parameters[i] = arg;
|
usedArgs.Add(arg);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (par[i].IsOptional) {
|
var ctorArg = TryGetService(paramType);
|
||||||
// this parameter is optional and not provided, use default value
|
if (ctorArg != null) {
|
||||||
parameters[i] = par[i].DefaultValue!;
|
resolvedParams[i] = ctorArg;
|
||||||
continue;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
var service = (TInterface?)Activator.CreateInstance(descriptor.ImplementationType, parameters);
|
throw new InvalidOperationException(
|
||||||
|
$"Cannot resolve parameter '{parameters[i].Name}' of type '{paramType.Name}' for service '{implementationType.Name}'.");
|
||||||
|
}
|
||||||
|
|
||||||
return service ?? throw new Exception($"Could not create instance of type {descriptor.ImplementationType}");
|
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,20 +8,28 @@ public class ServiceDescriptor
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the type of the service to be provided.
|
/// Gets or sets the type of the service to be provided.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public required Type ServiceType { get; set; }
|
public required Type ServiceType { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the concrete type that implements the service.
|
/// Gets or sets the concrete type that implements the service.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public required Type ImplementationType { get; set; }
|
public required Type ImplementationType { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the lifetime of the service (e.g., Singleton or Transient).
|
/// Gets or sets the lifetime of the service (e.g., Singleton or Transient).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public required ServiceLifetime Lifetime { get; set; }
|
public required ServiceLifetime Lifetime { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Arguments to be passed to the constructor of the implementation type.
|
/// Arguments to be passed to the constructor of the implementation type.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public List<object>? Arguments { get; set; }
|
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})";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
<LangVersion>latest</LangVersion>
|
<LangVersion>latest</LangVersion>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<Version>0.0.1.6-alpha</Version>
|
<Version>0.0.2.1-alpha</Version>
|
||||||
<Title>Syrette </Title>
|
<Title>Syrette </Title>
|
||||||
<Authors>Lorefice Samuele</Authors>
|
<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>
|
<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>
|
<Company>Samuele Lorefice</Company>
|
||||||
<Deterministic>true</Deterministic>
|
<Deterministic>true</Deterministic>
|
||||||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||||
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
|
<TargetFrameworks>net10.0;net8.0</TargetFrameworks>
|
||||||
<EmbedUntrackedSources>true</EmbedUntrackedSources>
|
<EmbedUntrackedSources>true</EmbedUntrackedSources>
|
||||||
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
|
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
Reference in New Issue
Block a user