Skip to content
Merged
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
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,7 +644,118 @@ 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);
Comment thread
AGiorgetti marked this conversation as resolved.
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);
Comment thread
AGiorgetti marked this conversation as resolved.
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"));
}
}
}

#pragma warning restore S3881 // "IDisposable" should be implemented correctly
Original file line number Diff line number Diff line change
Expand Up @@ -158,8 +158,8 @@ public static (IServiceCollection ServiceCollection, List<ServiceDescriptor> Ope
case ServiceLifetime.Transient
when (descriptor.IsKeyedService && descriptor.KeyedImplementationType?.IsGenericTypeDefinition == true)
|| (!descriptor.IsKeyedService && descriptor.ImplementationType?.IsGenericTypeDefinition == true):
if ((descriptor.IsKeyedService && typeof(IDisposable).IsAssignableFrom(descriptor.KeyedImplementationType))
|| (!descriptor.IsKeyedService && typeof(IDisposable).IsAssignableFrom(descriptor.ImplementationType)))
if ((descriptor.IsKeyedService && IsDisposableType(descriptor.KeyedImplementationType))
|| (!descriptor.IsKeyedService && IsDisposableType(descriptor.ImplementationType)))
{
if (throwOnOpenGenericTransientDisposable)
{
Expand All @@ -175,9 +175,9 @@ public static (IServiceCollection ServiceCollection, List<ServiceDescriptor> Ope
break;
case ServiceLifetime.Transient
when (descriptor is { IsKeyedService: true, KeyedImplementationType: not null }
&& typeof(IDisposable).IsAssignableFrom(descriptor.KeyedImplementationType))
&& IsDisposableType(descriptor.KeyedImplementationType))
|| (descriptor is { IsKeyedService: false, ImplementationType: not null }
&& typeof(IDisposable).IsAssignableFrom(descriptor.ImplementationType)):
&& IsDisposableType(descriptor.ImplementationType)):
collection.Add(CreatePatchedDescriptor(descriptor, allowSingletonToResolveTransientDisposables));
break;
case ServiceLifetime.Transient
Expand Down Expand Up @@ -214,10 +214,10 @@ bool allowSingletonToResolveTransientDisposables
//check the ResolutionContext to see if the service is being resolved by a singleton
//if it is, then it's safe to resolve the transient disposable service
if (sp.GetIsRootScope()
&& originalResult is IDisposable d
&& (originalResult is IDisposable || originalResult is IAsyncDisposable)
&& !IsResolvedBySingleton(sp, allowSingletonToResolveTransientDisposables))
{
ThrowTransientDisposableException(original.ServiceKey, original.ServiceType, d.GetType(), isFactory: true);
ThrowTransientDisposableException(original.ServiceKey, original.ServiceType, originalResult.GetType(), isFactory: true);
}

return originalResult;
Expand All @@ -243,10 +243,10 @@ bool allowSingletonToResolveTransientDisposables
//check the ResolutionContext to see if the service is being resolved by a singleton
//if it is, then it's safe to resolve the transient disposable service
if (sp.GetIsRootScope()
&& originalResult is IDisposable d
&& (originalResult is IDisposable || originalResult is IAsyncDisposable)
&& !IsResolvedBySingleton(sp, allowSingletonToResolveTransientDisposables))
{
ThrowTransientDisposableException(original.ServiceKey, original.ServiceType, d.GetType(), isFactory: true);
ThrowTransientDisposableException(original.ServiceKey, original.ServiceType, originalResult.GetType(), isFactory: true);
}

return originalResult;
Expand Down Expand Up @@ -390,6 +390,9 @@ private static void ThrowTransientDisposableException(object? serviceKey, Type?
}
#endif

private static bool IsDisposableType(Type? type) =>
type != null && (typeof(IDisposable).IsAssignableFrom(type) || typeof(IAsyncDisposable).IsAssignableFrom(type));

private static bool IsResolvedBySingleton(
IServiceProvider sp,
bool allowSingletonToResolveTransientDisposables
Expand Down