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
4 changes: 4 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@

### Breaking Changes

- Open-generic startup warnings now use the root `ILoggerFactory` rather than a directly registered `ILogger<ServiceProviderFactory>`. Configure standard logging (for example, `AddLogging`) to receive them. Warning-delivery failures no longer fail provider creation; warnings are best-effort [#90](https://github.com/PrimordialCode/Mammoth.Extensions.DependencyInjection/issues/90).

- Raise `Microsoft.Extensions.DependencyInjection` to a minimum of **10.0.0**, including the upstream keyed enumerable/open-generic cache identity fix ([dotnet/runtime#113343](https://github.com/dotnet/runtime/pull/113343), [#46](https://github.com/PrimordialCode/Mammoth.Extensions.DependencyInjection/issues/46)). Raise its required `Microsoft.Bcl.AsyncInterfaces` dependency to **10.0.0**. Consumers pinning DI 8.x/9.x must upgrade; all existing library target frameworks remain supported.

### Bug Fixes

- Remove the temporary startup-warning scope and return the provider even when optional warning delivery fails, preserving ownership of logging dependencies until provider disposal and avoiding forced synchronous disposal of async-only scoped loggers [#90](https://github.com/PrimordialCode/Mammoth.Extensions.DependencyInjection/issues/90).

- Preserve native DI constructor preference and parameter-type ambiguity rules when transient-disposable diagnostics instrument ordinary type registrations, including keyed context, optional defaults, null factory results and disposal ownership [#88](https://github.com/PrimordialCode/Mammoth.Extensions.DependencyInjection/issues/88).

- Build provider metadata on a private collection copy so repeated or failed builds leave caller registrations unchanged, avoid accumulating support services, and keep each provider's discovery and compatibility metadata independent [#64](https://github.com/PrimordialCode/Mammoth.Extensions.DependencyInjection/issues/64).
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -296,9 +296,11 @@ Instead of re-implementing a new ServiceProvider from scratch, this approach mod
Options:

- AllowSingletonToResolveTransientDisposables: Defaults to false. If true, permits the transient when the tracked ancestor chain contains a singleton. Resolution frames are isolated between execution-context branches. A task inherits the ancestry captured when it is scheduled, even if it outlives the originating factory; that inherited singleton ancestor still grants this exemption.
- ThrowOnOpenGenericTransientDisposable: Rejects recognized disposable open-generic type registrations at build time, naming the implementation type in the exception for both keyed and unkeyed registrations. Applies to IDisposable and IAsyncDisposable implementations. When false, these registrations instead produce warnings if a logger is available.
- ThrowOnOpenGenericTransientDisposable: Rejects recognized disposable open-generic type registrations at build time, naming the implementation type in the exception for both keyed and unkeyed registrations. Applies to IDisposable and IAsyncDisposable implementations. When false, these registrations instead produce best-effort warnings through the root ILoggerFactory, if registered (normally via AddLogging).
- DetectIncorrectUsageOfTransientDisposablesExclusionPatterns: list of Regex patterns to exclude services from detection, matching registrations bypass the diagnostic check. All diagnostic flags default to false. Factory-created disposable objects can exist before a diagnostic throws; normal scope disposal remains necessary.

Open-generic startup warnings use the `Mammoth.Extensions.DependencyInjection.ServiceProviderFactory` logging category and event ID 1. They do not create a temporary scope or resolve `ILogger<ServiceProviderFactory>` from DI. A direct registration of that logger alone no longer supplies these warnings; configure standard `ILoggerFactory` logging instead. Failures while resolving the logging factory, creating the logger or writing a warning are ignored, so a warning may be lost. Provider construction, validation and strict open-generic rejection still propagate their errors. Services created while attempting warning delivery remain owned by the returned provider; dispose it normally, using `DisposeAsync` when its services require asynchronous disposal.

###### IsRegistered extension methods

Additional methods for `IServiceProvider`:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Testing;
using System.Text.RegularExpressions;

namespace Mammoth.Extensions.DependencyInjection.Tests;
Expand Down Expand Up @@ -35,19 +34,19 @@ public void RejectionNamesTheImplementation(bool keyed, Type implementationType)
public async Task WarningModeNamesTheImplementationAndAllowsResolution(bool keyed, Type implementationType)
{
var services = CreateServices(keyed, implementationType, ServiceLifetime.Transient);
var logger = new FakeLogger<ServiceProviderFactory>();
services.AddSingleton<ILogger<ServiceProviderFactory>>(logger);
services.AddLogging(builder => builder.AddFakeLogging());
var options = CreateOptions();
options.ThrowOnOpenGenericTransientDisposable = false;

await using var provider = ServiceProviderFactory.CreateServiceProvider(services, options);

Assert.AreEqual(1, logger.Collector.Count);
Assert.AreEqual(LogLevel.Warning, logger.LatestRecord.Level);
var collector = provider.GetFakeLogCollector();
Assert.AreEqual(1, collector.Count);
Assert.AreEqual(LogLevel.Warning, collector.LatestRecord.Level);
Assert.AreEqual(
$"Open generic transient disposable registration detected, ServiceKey: {(keyed ? "resource" : "(null)")}, " +
$"ServiceType: {typeof(IResource<>)}, ImplementationType: {implementationType}",
logger.LatestRecord.Message);
collector.LatestRecord.Message);
await AssertResolution(provider, keyed, implementationType, ServiceLifetime.Transient);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
using Mammoth.Extensions.DependencyInjection.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Testing;

namespace Mammoth.Extensions.DependencyInjection.Tests
{
Expand Down Expand Up @@ -629,9 +628,7 @@ public void Register_OpenGeneric_TransientDisposable_WithValidation_Throws()
public void Register_OpenGeneric_TransientDisposable_WithoutValidation_LogsError()
{
var serviceCollection = CreateServiceCollection();
// register a fake logger
var fakeLogger = new FakeLogger<ServiceProviderFactory>();
serviceCollection.AddSingleton<ILogger<ServiceProviderFactory>>(fakeLogger);
serviceCollection.AddLogging(builder => builder.AddFakeLogging());
using var sp = ServiceProviderFactory.CreateServiceProvider(serviceCollection,
new ExtendedServiceProviderOptions
{
Expand All @@ -640,9 +637,10 @@ public void Register_OpenGeneric_TransientDisposable_WithoutValidation_LogsError
ValidateOnBuild = true,
ValidateScopes = true
});
Assert.AreEqual(1, fakeLogger.Collector.Count);
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);
var collector = sp.GetFakeLogCollector();
Assert.AreEqual(1, collector.Count);
Assert.AreEqual(LogLevel.Warning, collector.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]", collector.LatestRecord.Message);
}

[TestMethod]
Expand Down
Loading