Compare commits
6 Commits
65f624a355
...
b9c4f65892
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b9c4f65892 | ||
|
|
fda62e30bc | ||
|
|
5dfee34bf5 | ||
|
|
8fc2fbc7f1 | ||
|
|
f8f814dd16 | ||
|
|
568ab548e5 |
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
|
||||
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,290 +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> GetServiceTypes<TServices>() =>
|
||||
descriptors.Where(d => d.ServiceType == typeof(TServices))
|
||||
.Select(d => d.ImplementationType).ToList();
|
||||
|
||||
/// <summary>
|
||||
/// Get all registered services for a given service type.
|
||||
/// </summary>
|
||||
/// <typeparam name="TService"></typeparam>
|
||||
public List<TService> GetServices<TService>() where TService : class =>
|
||||
descriptors.Where(d => d.ServiceType == typeof(TService))
|
||||
.Select(d => (TService)GetService(d.ImplementationType, d.Arguments?.ToArray())).ToList();
|
||||
.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>()
|
||||
where TInterface : class
|
||||
where TImplementation : class, TInterface {
|
||||
descriptors.Add(new() {
|
||||
ServiceType = typeof(TInterface),
|
||||
ImplementationType = typeof(TImplementation),
|
||||
Lifetime = ServiceLifetime.Singleton
|
||||
});
|
||||
AddDescriptor(typeof(TInterface), typeof(TImplementation), ServiceLifetime.Singleton, null);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a singleton service with its implementation.
|
||||
/// </summary>
|
||||
/// <param name="args">Arguments to be passed to the constructor, in order of appearance if of the same type.</param>
|
||||
/// <typeparam name="TInterface">Interface the service is implementing</typeparam>
|
||||
/// <typeparam name="TImplementation">Implementation type of the service</typeparam>
|
||||
public ServiceContainer AddSingleton<TInterface, TImplementation>(params object[] args)
|
||||
where TInterface : class
|
||||
where TImplementation : class, TInterface {
|
||||
descriptors.Add(new() {
|
||||
ServiceType = typeof(TInterface),
|
||||
ImplementationType = typeof(TImplementation),
|
||||
Lifetime = ServiceLifetime.Singleton,
|
||||
Arguments = args.ToList()
|
||||
});
|
||||
AddDescriptor(typeof(TInterface), typeof(TImplementation), ServiceLifetime.Singleton, args);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a singleton service where the service type is the same as the implementation type.
|
||||
/// </summary>
|
||||
/// <typeparam name="TClass">Class type of the service</typeparam>
|
||||
public ServiceContainer AddSingleton<TClass>()
|
||||
where TClass : class {
|
||||
descriptors.Add(new() {
|
||||
ServiceType = typeof(TClass),
|
||||
ImplementationType = typeof(TClass),
|
||||
Lifetime = ServiceLifetime.Singleton
|
||||
});
|
||||
AddDescriptor(typeof(TClass), typeof(TClass), ServiceLifetime.Singleton, null);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a singleton service with its implementation.
|
||||
/// </summary>
|
||||
/// <param name="args">Arguments to be passed to the constructor, in order of appearance if of the same type.</param>
|
||||
/// <typeparam name="TImplementation">Implementation type of the service</typeparam>
|
||||
public ServiceContainer AddSingleton<TImplementation>(params object[] args)
|
||||
where TImplementation : class {
|
||||
descriptors.Add(new() {
|
||||
ServiceType = typeof(TImplementation),
|
||||
ImplementationType = typeof(TImplementation),
|
||||
Lifetime = ServiceLifetime.Singleton,
|
||||
Arguments = args.ToList()
|
||||
});
|
||||
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() {
|
||||
ServiceType = typeof(TInterface),
|
||||
ImplementationType = typeof(TImplementation),
|
||||
Lifetime = ServiceLifetime.Transient
|
||||
});
|
||||
AddDescriptor(typeof(TInterface), typeof(TImplementation), ServiceLifetime.Transient, null);
|
||||
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)
|
||||
where TInterface : class
|
||||
where TImplementation : class, TInterface {
|
||||
descriptors.Add(new() {
|
||||
ServiceType = typeof(TInterface),
|
||||
ImplementationType = typeof(TImplementation),
|
||||
Lifetime = ServiceLifetime.Transient,
|
||||
Arguments = args.ToList()
|
||||
});
|
||||
AddDescriptor(typeof(TInterface), typeof(TImplementation), ServiceLifetime.Transient, args);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a transient service where the service type is the same as the implementation type.
|
||||
/// </summary>
|
||||
/// <typeparam name="TClass">Class type of the service</typeparam>
|
||||
public ServiceContainer AddTransient<TClass>()
|
||||
where TClass : class {
|
||||
descriptors.Add(new() {
|
||||
ServiceType = typeof(TClass),
|
||||
ImplementationType = typeof(TClass),
|
||||
Lifetime = ServiceLifetime.Transient
|
||||
});
|
||||
AddDescriptor(typeof(TClass), typeof(TClass), ServiceLifetime.Transient, null);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a transient service where the service type is the same as the implementation type.
|
||||
/// </summary>
|
||||
/// <param name="args">Arguments to be passed to the constructor, in order of appearance if of the same type.</param>
|
||||
/// <typeparam name="TClass">Class type of the service</typeparam>
|
||||
public ServiceContainer AddTransient<TClass>(params object[] args)
|
||||
where TClass : class {
|
||||
descriptors.Add(new() {
|
||||
ServiceType = typeof(TClass),
|
||||
ImplementationType = typeof(TClass),
|
||||
Lifetime = ServiceLifetime.Transient,
|
||||
Arguments = args.ToList()
|
||||
});
|
||||
AddDescriptor(typeof(TClass), typeof(TClass), ServiceLifetime.Transient, args);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves and returns an instance of the requested service type.
|
||||
/// </summary>
|
||||
/// <param name="serviceType">Type of the service that's being requested</param>
|
||||
/// <param name="args">arguments to pass to the constructor of the service</param>
|
||||
/// <remarks> you can't call generic methods with an unknown type at compile time
|
||||
/// so we use reflection to call the generic GetService{T} method with the provided
|
||||
/// type Basically we build the method GetService{serviceType}() at runtime and then call it.</remarks>
|
||||
/// <returns>An object that is the instantiated service type</returns>
|
||||
public object GetService(Type serviceType, object[]? args = null) {
|
||||
List<Type> arguments = [serviceType];
|
||||
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.");
|
||||
}
|
||||
|
||||
if (args != null) arguments.AddRange(args.ToList().Select(a => a.GetType()));
|
||||
descriptors.Add(new ServiceDescriptor {
|
||||
ServiceType = serviceType,
|
||||
ImplementationType = implementationType,
|
||||
Lifetime = lifetime,
|
||||
Arguments = args?.ToList()
|
||||
});
|
||||
}
|
||||
|
||||
var method = typeof(ServiceContainer)
|
||||
.GetMethod(nameof(GetService))!
|
||||
.MakeGenericMethod(arguments.ToArray());
|
||||
public object GetService(Type serviceType, object[]? args = null) {
|
||||
return ResolveService(serviceType, args);
|
||||
}
|
||||
|
||||
return method.Invoke(this, args)!;
|
||||
public TService GetService<TService>() {
|
||||
return (TService)ResolveService(typeof(TService), null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// tries to resolve and return an instance of the requested service type. Returns null if it fails.
|
||||
/// </summary>
|
||||
/// <param name="serviceType">Type of the service that's being requested</param>
|
||||
/// <param name="args">arguments to pass to the constructor of the service</param>
|
||||
/// <remarks> you can't call generic methods with an unknown type at compile time
|
||||
/// so we use reflection to call the generic GetService{T} method with the provided
|
||||
/// type Basically we build the method GetService{serviceType}() at runtime and then call it.</remarks>
|
||||
/// <returns>An object that is the instantiated service type or null if not found</returns>
|
||||
public object? TryGetService(Type serviceType, object[]? args = null) {
|
||||
try {
|
||||
return GetService(serviceType, args);
|
||||
} catch {
|
||||
return ResolveService(serviceType, args);
|
||||
}
|
||||
catch (InvalidOperationException) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves and returns an instance of the requested service type.
|
||||
/// </summary>
|
||||
/// <typeparam name="TService">Interface type of the service being requested</typeparam>
|
||||
/// <returns>Resolved service instance</returns>
|
||||
public TService GetService<TService>() {
|
||||
var descriptor = descriptors.FirstOrDefault(d => d.ServiceType == typeof(TService) || d.ImplementationType == typeof(TService));
|
||||
private object ResolveService(Type serviceType, object[]? resolutionArgs) {
|
||||
var descriptor = descriptors.FirstOrDefault(d =>
|
||||
d.ServiceType == serviceType || d.ImplementationType == serviceType);
|
||||
|
||||
if (descriptor == null) throw new Exception($"Service of type {typeof(TService)} not registered.");
|
||||
if (descriptor == null) {
|
||||
throw new InvalidOperationException(
|
||||
$"Service of type '{serviceType.Name}' not registered.");
|
||||
}
|
||||
|
||||
var mergedArgs = descriptor.Arguments != null
|
||||
? new List<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 par = descriptor.Arguments ?? new List<object>();
|
||||
int max = -1;
|
||||
ConstructorInfo? bestCtor = null;
|
||||
int max = -1;
|
||||
|
||||
foreach (var ctor in ctors) {
|
||||
var parameters = ctor.GetParameters();
|
||||
//check if all parameters are registered services or optional or have been provided as arguments
|
||||
if (parameters.Any(p => descriptors.All(d => d.ServiceType != p.ParameterType) && par.All(a => a.GetType() != p.ParameterType) && !p.IsOptional))
|
||||
|
||||
if (parameters.Any(p =>
|
||||
descriptors.All(d => d.ServiceType != p.ParameterType) &&
|
||||
mergedArgs.All(a => !p.ParameterType.IsAssignableFrom(a.GetType())) &&
|
||||
!p.IsOptional)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
//check if this constructor has more registered parameters than the previous best
|
||||
int satisfiedParams = parameters.Count(p => descriptors.Any(d => d.ServiceType == p.ParameterType));
|
||||
satisfiedParams += par.Count(arg => parameters.Any(p => p.ParameterType == arg.GetType()));
|
||||
int satisfied = parameters.Count(p =>
|
||||
descriptors.Any(d => d.ServiceType == p.ParameterType));
|
||||
|
||||
if (satisfiedParams > max) {
|
||||
max = satisfiedParams;
|
||||
int argSatisfied = 0;
|
||||
|
||||
foreach (var param in parameters) {
|
||||
if (!descriptors.Any(d => d.ServiceType == param.ParameterType) &&
|
||||
mergedArgs.Any(a => param.ParameterType.IsAssignableFrom(a.GetType()))) {
|
||||
argSatisfied++;
|
||||
}
|
||||
}
|
||||
|
||||
satisfied += argSatisfied;
|
||||
|
||||
if (satisfied > max) {
|
||||
max = satisfied;
|
||||
bestCtor = ctor;
|
||||
}
|
||||
}
|
||||
|
||||
if (bestCtor == null)
|
||||
throw new Exception($"Cannot create service of type {typeof(TService)}. No suitable constructor found.");
|
||||
|
||||
// Transient: create a new instance each time
|
||||
if (descriptor.Lifetime != ServiceLifetime.Singleton) {
|
||||
var service = Instantiate<TService>(descriptor, bestCtor);
|
||||
return service;
|
||||
if (bestCtor == null) {
|
||||
throw new InvalidOperationException(
|
||||
$"Cannot create service of type '{serviceType.Name}'. No suitable constructor found.");
|
||||
}
|
||||
|
||||
// Singleton: return existing instance
|
||||
if (singletons.TryGetValue(descriptor.ServiceType, out object? singleton)) return (TService)singleton;
|
||||
if (descriptor.Lifetime == ServiceLifetime.Singleton) {
|
||||
lock (singletonLock) {
|
||||
if (singletons.TryGetValue(descriptor.ImplementationType, out var singleton)) {
|
||||
return singleton;
|
||||
}
|
||||
|
||||
// or create a new one if not yet created.
|
||||
var newSingleton = Instantiate<TService>(descriptor, bestCtor);
|
||||
singletons[descriptor.ServiceType] = newSingleton!;
|
||||
return newSingleton;
|
||||
var instance = Instantiate(descriptor.ImplementationType, bestCtor, mergedArgs);
|
||||
singletons[descriptor.ImplementationType] = instance;
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
|
||||
return Instantiate(descriptor.ImplementationType, bestCtor, mergedArgs);
|
||||
}
|
||||
|
||||
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.");
|
||||
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>();
|
||||
|
||||
List<ParameterInfo> par;
|
||||
List<object> args = descriptor.Arguments != null ? new List<object>(descriptor.Arguments) : new List<object>();
|
||||
for (var i = 0; i < parameters.Length; i++) {
|
||||
var paramType = parameters[i].ParameterType;
|
||||
|
||||
if (ctor == null)
|
||||
par = descriptor.ImplementationType
|
||||
.GetConstructors().Single()
|
||||
.GetParameters()
|
||||
//.Select(p => p.ParameterType)
|
||||
.ToList();
|
||||
else
|
||||
par = ctor.GetParameters()
|
||||
//.Select(p => p.ParameterType)
|
||||
.ToList();
|
||||
var arg = args.FirstOrDefault(a =>
|
||||
!usedArgs.Contains(a) && paramType.IsAssignableFrom(a.GetType()));
|
||||
|
||||
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);
|
||||
if (arg != null) {
|
||||
// this parameter is satisfied by a registered service
|
||||
parameters[i] = arg;
|
||||
resolvedParams[i] = arg;
|
||||
usedArgs.Add(arg);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (par[i].IsOptional) {
|
||||
// this parameter is optional and not provided, use default value
|
||||
parameters[i] = par[i].DefaultValue!;
|
||||
var ctorArg = TryGetService(paramType);
|
||||
if (ctorArg != null) {
|
||||
resolvedParams[i] = ctorArg;
|
||||
continue;
|
||||
}
|
||||
throw new Exception($"Cannot resolve parameter {par[i].Name} of type {par[i].ParameterType} for service {descriptor.ImplementationType}");
|
||||
|
||||
if (parameters[i].IsOptional) {
|
||||
resolvedParams[i] = parameters[i].DefaultValue;
|
||||
continue;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException(
|
||||
$"Cannot resolve parameter '{parameters[i].Name}' of type '{paramType.Name}' for service '{implementationType.Name}'.");
|
||||
}
|
||||
|
||||
var service = (TInterface?)Activator.CreateInstance(descriptor.ImplementationType, parameters);
|
||||
var instance = Activator.CreateInstance(implementationType, resolvedParams);
|
||||
|
||||
return service ?? throw new Exception($"Could not create instance of type {descriptor.ImplementationType}");
|
||||
return instance ??
|
||||
throw new InvalidOperationException(
|
||||
$"Could not create instance of type '{implementationType.Name}'.");
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
foreach (var disposable in singletons.Values.OfType<IDisposable>()) {
|
||||
disposable.Dispose();
|
||||
}
|
||||
|
||||
singletons.Clear();
|
||||
descriptors.Clear();
|
||||
}
|
||||
}
|
||||
@@ -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