Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@

## vNext

## 0.7.1

### Bug Fixes

- `IServiceCollection` lifetime check methods (`IsTransientServiceRegistered`, `IsScopedServiceRegistered`, `IsSingletonServiceRegistered`, and their keyed variants) now return `false` instead of throwing `InvalidOperationException` when the service is not registered.
- Fixed `ResolutionContext` stack corruption in `PatchForResolutionContextTracking` when a factory delegate throws an exception. Push/Pop calls are now wrapped in `try/finally` for all 4 registration paths (non-keyed/keyed × factory/type).
- Fixed bidirectional `IsAssignableFrom` check in `GetServiceDescriptors` method that incorrectly returned unrelated base-type registrations [#19](https://github.com/PrimordialCode/Mammoth.Extensions.DependencyInjection/issues/19).
- The method now uses unidirectional matching: `serviceType == serviceDescriptor.ServiceType || serviceType.IsAssignableFrom(serviceDescriptor.ServiceType)`.
- This fixes incorrect lifetime checks in methods like `IsTransientServiceRegistered`, `IsSingletonServiceRegistered`, etc., which depend on `.Last()` to select the correct registration.
- Fixed missing `return` statements in `TryAdd*` overload methods with empty `DependsOn` array, which caused unnecessary reflection calls and potential double-registration attempts [#18](https://github.com/PrimordialCode/Mammoth.Extensions.DependencyInjection/issues/18).

## 0.7.0

- Decorators: removed reflection-based proxy creation (`Reflection.Emit`) and allow class decoration [#11](https://github.com/PrimordialCode/Mammoth.Extensions.DependencyInjection/issues/11).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,45 @@ public void Dispose()
}
}

public class TransientAsyncDisposable : IAsyncDisposable
{
public ValueTask DisposeAsync()
{
GC.SuppressFinalize(this);
return default;
}
}

public class AsyncDisposableConsumer : IAsyncDisposable
{
public TransientAsyncDisposable TransientAsyncDisposable { get; }

public AsyncDisposableConsumer(TransientAsyncDisposable transientAsyncDisposable)
{
TransientAsyncDisposable = transientAsyncDisposable;
}

public ValueTask DisposeAsync()
{
GC.SuppressFinalize(this);
return default;
}
}

public class AsyncDisposableConsumerFactory
{
#pragma warning disable IDE0079 // Remove unnecessary suppression
#pragma warning disable CA1822 // Mark members as static
#pragma warning disable S2325 // Methods and properties that don't access instance data should be static
public AsyncDisposableConsumer Build(IServiceProvider sp)
#pragma warning restore S2325 // Methods and properties that don't access instance data should be static
#pragma warning restore CA1822 // Mark members as static
#pragma warning restore IDE0079 // Remove unnecessary suppression
{
return new AsyncDisposableConsumer(sp.GetRequiredService<TransientAsyncDisposable>());
}
}

private static ServiceCollection CreateServiceCollection()
{
var serviceCollection = new ServiceCollection();
Expand Down Expand Up @@ -605,6 +644,198 @@ public void Register_OpenGeneric_TransientDisposable_WithoutValidation_LogsError
Assert.AreEqual(LogLevel.Warning, fakeLogger.LatestRecord.Level);
Assert.AreEqual("Open generic transient disposable registration detected, ServiceKey: (null), ServiceType: Mammoth.Extensions.DependencyInjection.Tests.DetectIncorrectUsageOfTransientDisposablesTests+ITransientOpenGeneric`1[T], ImplementationType: Mammoth.Extensions.DependencyInjection.Tests.DetectIncorrectUsageOfTransientDisposablesTests+TransientOpenGeneric`1[T]", fakeLogger.LatestRecord.Message);
}

[TestMethod]
public void Resolve_TransientAsyncDisposable_InRootScope_WithValidation_Throws()
{
var serviceCollection = new ServiceCollection();
serviceCollection.AddTransient<TransientAsyncDisposable>();
using var sp = ServiceProviderFactory.CreateServiceProvider(serviceCollection,
new ExtendedServiceProviderOptions
{
DetectIncorrectUsageOfTransientDisposables = true,
ValidateOnBuild = true,
ValidateScopes = true
});
Assert.ThrowsExactly<InvalidOperationException>(() => sp.GetService<TransientAsyncDisposable>());
}

[TestMethod]
public async Task Resolve_TransientAsyncDisposable_InScope_WithValidation_NoMemoryLeak()
{
var serviceCollection = new ServiceCollection();
serviceCollection.AddTransient<TransientAsyncDisposable>();
var sp = ServiceProviderFactory.CreateServiceProvider(serviceCollection,
new ExtendedServiceProviderOptions
{
DetectIncorrectUsageOfTransientDisposables = true,
ValidateOnBuild = true,
ValidateScopes = true
});
await using (sp)
{
await using var scope = sp.CreateAsyncScope();
var consumer = scope.ServiceProvider.GetService<TransientAsyncDisposable>();
Assert.IsNotNull(consumer);
Assert.IsFalse(scope.ServiceProvider.GetIsRootScope());
Assert.IsTrue(scope.ServiceProvider.GetDisposables().Contains(consumer));
Assert.IsTrue(sp.GetIsRootScope());
Assert.IsFalse(sp.GetDisposables().Contains(consumer));
}
}

[TestMethod]
public void Resolve_AsyncDisposableConsumer_using_factory_InRootScope_WithValidation_Throws()
{
var serviceCollection = new ServiceCollection();
serviceCollection.AddTransient<TransientAsyncDisposable>();
serviceCollection.AddSingleton<AsyncDisposableConsumerFactory>();
serviceCollection.AddTransient(sp => sp.GetRequiredService<AsyncDisposableConsumerFactory>().Build(sp));
using var spProvider = ServiceProviderFactory.CreateServiceProvider(serviceCollection,
new ExtendedServiceProviderOptions
{
DetectIncorrectUsageOfTransientDisposables = true,
ValidateOnBuild = true,
ValidateScopes = true
});
Assert.ThrowsExactly<InvalidOperationException>(() => spProvider.GetService<AsyncDisposableConsumer>());
}

[TestMethod]
public async Task Resolve_AsyncDisposableConsumer_using_factory_InScope_WithValidation_NoMemoryLeak()
{
var serviceCollection = new ServiceCollection();
serviceCollection.AddTransient<TransientAsyncDisposable>();
serviceCollection.AddSingleton<AsyncDisposableConsumerFactory>();
serviceCollection.AddTransient(sp => sp.GetRequiredService<AsyncDisposableConsumerFactory>().Build(sp));
var spProvider = ServiceProviderFactory.CreateServiceProvider(serviceCollection,
new ExtendedServiceProviderOptions
{
DetectIncorrectUsageOfTransientDisposables = true,
ValidateOnBuild = true,
ValidateScopes = true
});
await using (spProvider)
{
await using var scope = spProvider.CreateAsyncScope();
var consumer = scope.ServiceProvider.GetService<AsyncDisposableConsumer>();
Assert.IsNotNull(consumer);
Assert.IsFalse(scope.ServiceProvider.GetIsRootScope());
Assert.IsTrue(scope.ServiceProvider.GetDisposables().Contains(consumer));
Assert.IsTrue(spProvider.GetIsRootScope());
Assert.IsFalse(spProvider.GetDisposables().Contains(consumer));
}
}

[TestMethod]
public void Resolve_KeyedTransientAsyncDisposable_InRootScope_WithValidation_Throws()
{
var serviceCollection = new ServiceCollection();
serviceCollection.AddKeyedTransient<TransientAsyncDisposable>("key");
using var sp = ServiceProviderFactory.CreateServiceProvider(serviceCollection,
new ExtendedServiceProviderOptions
{
DetectIncorrectUsageOfTransientDisposables = true,
ValidateOnBuild = true,
ValidateScopes = true
});
Assert.ThrowsExactly<InvalidOperationException>(() => sp.GetKeyedService<TransientAsyncDisposable>("key"));
}

[TestMethod]
public void Resolve_KeyedTransientAsyncDisposable_using_factory_InRootScope_WithValidation_Throws()
{
var serviceCollection = new ServiceCollection();
serviceCollection.AddKeyedTransient<TransientAsyncDisposable>("key", (sp, key) => new TransientAsyncDisposable());
using var spProvider = ServiceProviderFactory.CreateServiceProvider(serviceCollection,
new ExtendedServiceProviderOptions
{
DetectIncorrectUsageOfTransientDisposables = true,
ValidateOnBuild = true,
ValidateScopes = true
});
Assert.ThrowsExactly<InvalidOperationException>(() => spProvider.GetKeyedService<TransientAsyncDisposable>("key"));
}

[TestMethod]
public void PatchForResolutionContextTracking_FactoryThrows_StackIsClean()
{
var serviceCollection = new ServiceCollection();
serviceCollection.AddTransient<Consumer>(sp => throw new InvalidOperationException("factory error"));

using var sp = ServiceProviderFactory.CreateServiceProvider(serviceCollection,
new ExtendedServiceProviderOptions
{
DetectIncorrectUsageOfTransientDisposables = true,
ValidateOnBuild = false,
ValidateScopes = true
});

Assert.ThrowsExactly<InvalidOperationException>(() => sp.GetService<Consumer>());
Assert.AreEqual(0, ResolutionContext.CurrentStack.Count);
}

[TestMethod]
public void PatchForResolutionContextTracking_KeyedFactoryThrows_StackIsClean()
{
var serviceCollection = new ServiceCollection();
serviceCollection.AddKeyedTransient<Consumer>("key", (sp, key) => throw new InvalidOperationException("keyed factory error"));

using var sp = ServiceProviderFactory.CreateServiceProvider(serviceCollection,
new ExtendedServiceProviderOptions
{
DetectIncorrectUsageOfTransientDisposables = true,
ValidateOnBuild = false,
ValidateScopes = true
});

Assert.ThrowsExactly<InvalidOperationException>(() => sp.GetKeyedService<Consumer>("key"));
Assert.AreEqual(0, ResolutionContext.CurrentStack.Count);
}

[TestMethod]
public void PatchForResolutionContextTracking_TypeCtorThrows_StackIsClean()
{
var serviceCollection = new ServiceCollection();
serviceCollection.AddTransient<ThrowingConstructor>();

using var sp = ServiceProviderFactory.CreateServiceProvider(serviceCollection,
new ExtendedServiceProviderOptions
{
DetectIncorrectUsageOfTransientDisposables = true,
ValidateOnBuild = false,
ValidateScopes = true
});

Assert.ThrowsExactly<InvalidOperationException>(() => sp.GetService<ThrowingConstructor>());
Assert.AreEqual(0, ResolutionContext.CurrentStack.Count);
}

[TestMethod]
public void PatchForResolutionContextTracking_KeyedTypeCtorThrows_StackIsClean()
{
var serviceCollection = new ServiceCollection();
serviceCollection.AddKeyedTransient<ThrowingConstructor>("key");

using var sp = ServiceProviderFactory.CreateServiceProvider(serviceCollection,
new ExtendedServiceProviderOptions
{
DetectIncorrectUsageOfTransientDisposables = true,
ValidateOnBuild = false,
ValidateScopes = true
});

Assert.ThrowsExactly<InvalidOperationException>(() => sp.GetKeyedService<ThrowingConstructor>("key"));
Assert.AreEqual(0, ResolutionContext.CurrentStack.Count);
}

private sealed class ThrowingConstructor
{
public ThrowingConstructor()
{
throw new InvalidOperationException("constructor error");
}
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,65 @@ public void Singleton_Resolve_DependsOn_Value()
Assert.AreEqual("val1", service.Dep);
}

[TestMethod]
public void TryAddSingleton_EmptyDependsOn_RegistersService()
{
var serviceCollection = new ServiceCollection();
var emptyDependsOn = Array.Empty<Dependency>();
serviceCollection.TryAddSingleton(typeof(SimpleService), emptyDependsOn);

Assert.AreEqual(1, serviceCollection.Count);
Assert.IsTrue(serviceCollection.IsServiceRegistered<SimpleService>());

using var serviceProvider = serviceCollection.BuildServiceProvider();
var service = serviceProvider.GetRequiredService<SimpleService>();
Assert.IsNotNull(service);
}

[TestMethod]
public void TryAddScoped_EmptyDependsOn_RegistersService()
{
var serviceCollection = new ServiceCollection();
var emptyDependsOn = Array.Empty<Dependency>();
serviceCollection.TryAddScoped<SimpleService>(emptyDependsOn);

Assert.AreEqual(1, serviceCollection.Count);
Assert.IsTrue(serviceCollection.IsServiceRegistered<SimpleService>());

using var serviceProvider = serviceCollection.BuildServiceProvider();
var service = serviceProvider.GetRequiredService<SimpleService>();
Assert.IsNotNull(service);
}

[TestMethod]
public void TryAddTransient_EmptyDependsOn_RegistersService()
{
var serviceCollection = new ServiceCollection();
var emptyDependsOn = Array.Empty<Dependency>();
serviceCollection.TryAddTransient<SimpleService>(emptyDependsOn);

Assert.AreEqual(1, serviceCollection.Count);
Assert.IsTrue(serviceCollection.IsServiceRegistered<SimpleService>());

using var serviceProvider = serviceCollection.BuildServiceProvider();
var service = serviceProvider.GetRequiredService<SimpleService>();
Assert.IsNotNull(service);
}

[TestMethod]
public void TryAddKeyedSingleton_EmptyDependsOn_RegistersService()
{
var serviceCollection = new ServiceCollection();
var emptyDependsOn = Array.Empty<Dependency>();
serviceCollection.TryAddKeyedSingleton<SimpleService>("key1", emptyDependsOn);

Assert.AreEqual(1, serviceCollection.Count);

using var serviceProvider = serviceCollection.BuildServiceProvider();
var service = serviceProvider.GetRequiredKeyedService<SimpleService>("key1");
Assert.IsNotNull(service);
}

[TestMethod]
public void Resolve_KeyedService_DependsOn_Parameter()
{
Expand Down Expand Up @@ -295,6 +354,10 @@ public OpenGenericServiceWithKeyedDep(IKeyedService keyedService)

public IKeyedService KeyedService { get; }
}

public class SimpleService
{
}
}
}

Expand Down
Loading
Loading